Shared pointer constness in comparison operator ==

瘦欲@ 提交于 2019-12-11 14:09:24

问题


I stumbled upon an unexpected behavior of a shared pointer I'm using.

The shared pointer implements reference counting and detaches (e.g. makes a copy of), if neccessary, the contained instance on non-const usage.
To achieve this, for each getter function the smart pointer has a const and a non-const version, for example: operator T *() and operator T const *() const.

Problem: Comparing the pointer value to nullptr leads to a detach.

Expected: I thought that the comparison operator would always invoke the const version.

Simplified example:
(This implementation doesn't have reference counting, but still shows the problem)

#include <iostream>

template<typename T>
class SharedPointer
{
public:
    inline operator T *() { std::cout << "Detached"; return d; }
    inline operator const T *() const { std::cout << "Not detached"; return d; }
    inline T *data() { std::cout << "Detached"; return d; }
    inline const T *data() const { std::cout << "Not detached"; return d; }
    inline const T *constData() const { std::cout << "Not detached"; return d; }

    SharedPointer(T *_d) : d(_d) { }

private:
    T *d;
};


int main(int argc, char *argv[])
{
    SharedPointer<int> testInst(new int(0));

    bool eq;

    std::cout << "nullptr  == testInst: ";
    eq = nullptr == testInst;
    std::cout << std::endl;
    // Output: nullptr  == testInst: Detached

    std::cout << "nullptr  == testInst.data(): ";
    eq = nullptr == testInst.data();
    std::cout << std::endl;
    // Output: nullptr  == testInst.data(): Detached

    std::cout << "nullptr  == testInst.constData(): ";
    eq = nullptr == testInst.constData();
    std::cout << std::endl;
    // Output: nullptr  == testInst.constData(): Not detached
}

Question 1: Why is the non-const version of the functions called when it should be sufficient to call the const version?

Question 2: Why can the non-const version be called anyways? Doesn't the comparison operator (especially comparing to the immutable nullptr) always operate on const references?


For the record:
The shared pointer I'm using is Qt's QSharedDataPointer holding a QSharedData-derived instance, but this question is not Qt-specific.


Edit:

In my understanding, nullptr == testInst would invoke

bool operator==(T const* a, T const* b)

(Because why should I compare non-const pointers?)

which should invoke:

inline operator const T *() const 

Further questions:

  • Why isn't it the default to use the const operator?
    • Is this because a function cannot be selected by the type of the return value alone?
      => This is answered in Calling a const function rather than its non-const version

So this question boils down to:

  • Why doesn't the default implementation of the comparison operator take the arguments as const refs and then call the const functions?
  • Can you maybe cite a c++ reference?

回答1:


When there exists an overload on const and non-const, the compiler will always call non-const version if the object you're using is non-const. Otherwise, when would the non-const version ever be invoked?

If you want to explicitly use the const versions, invoke them through a const reference:

const SharedPointer<int>& constRef = testInst;
eq = nullptr == constRef;

In the context of Qt's QSharedDataPointer, you can also use the constData function explicitly whenever you need a pointer.

For the intended usage of QSharedDataPointer, this behavior is not usually a problem. It is meant to be a member of a facade class, and thus used only from its member functions. Those member functions that don't need modification (and thus don't need detaching) are expected to be const themselves, making the member access to the pointer be in a const context and thus not detach.

Edit to answer the edit:

In my understanding, nullptr == testInst would invoke

bool operator==(T const* a, T const* b)

This understanding is incorrect. Overload resolutions for operators is rather complex, with a big set of proxy signatures for the built-in version of the operator taking part in the resolution. This process is described in [over.match.oper] and [over.built] in the standard.

Specifically, the relevant built-in candidates for equality are defined in [over.built]p16 and 17. These rules say that for every pointer type T, an operator ==(T, T) exists. Now, both int* and const int* are pointer types, so the two relevant signatures are operator ==(int*, int*) and operator ==(const int*, const int*). (There's also operator ==(std::nullptr_t, std::nullptr_t), but it won't be selected.)

To distinguish between the two overloads, the compiler has to compare conversion sequences. For the first argument, nullptr_t -> int* and nullptr_t -> const int* are both identical; they are pointer conversions. Adding the const to one of the pointers is subsumed. (See [conv.ptr].) For the second argument, the conversions are SharedPointer<int> -> int* and SharedPointer<int> -> const int*, respectively. The first of these is a user-defined conversion, invoking operator int*(), with no further conversions necessary. The second is a user-defined conversion, invoking operator const int*() const, which necessitates a qualification conversion first in order to call the const version. Therefore, the non-const version is preferred.




回答2:


Maybe this code will allow you to understand what happens:

class X {
   public:
      operator int * () { std::cout << "1\n"; return nullptr; }
      operator const int * () { std::cout << "2\n"; return nullptr; }
      operator int * () const { std::cout << "3\n"; return nullptr; }
      operator const int * () const { std::cout << "4\n"; return nullptr; }
};

int main() {
   X x;
   const X & rcx = x;

   int* pi1 = x;
   const int* pi2 = x;
   int* pi3 = rcx;
   const int* pi4 = rcx;
}

The output is

1
2
3
4

If the const object (or reference to it) is casted, the const cast operator (case 3 and 4) is choosen, and vice versa.




回答3:


This is because of how the expression testInst == nullptr is resolved:

  1. Let's look at the types:
    testInst is of type SharedPointer<int>.
    nullptr is (for the sake of simplification) of type T* or void*, depending on the use case.
    So the expression reads SharedPointer<int> == int*.
  2. We need to have equal types to invoke a comparison operator. There are two possibilities:
    1. Resolve to int* == int*.
      This involves a call to operator int *() or operator int const *() const.
      [citation needed]
    2. Resolve to SharedPointer<int> == SharedPointer<int>
      This involves a call to SharedPointer(nullptr).
  3. Because the second option would create a new object, and the first one doesn't, the first option is the better match. [citation needed]
  4. Now before resolving bool operator==(int [const] *a, int [const] *b) (the [const] is irrelevant), testInst must be converted to int*.
    This involves a call to the conversion operator int *() or operator int const *() const.
    Here, the non-const version will be called because testInst is not const. [citation needed]

I created a suggestion to add comparison operators for T* to QSharedDataPointer<T> at Qt Bugs: https://bugreports.qt.io/browse/QTBUG-66946



来源:https://stackoverflow.com/questions/49168637/shared-pointer-constness-in-comparison-operator

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!