0

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:

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?
Martin Hennings
  • 16,418
  • 9
  • 48
  • 68
  • Look at what `std::vector::operator[]` does. – Passer By Mar 08 '18 at 08:38
  • `testInst` needs to be cast into a pointer. Since `testInst` is a non-const object, its non-const cast operator is naturally selected. – Daniel Langr Mar 08 '18 at 08:46
  • @DanielLangr It's the *naturally* that confuses me. See the edit for detailed questions. – Martin Hennings Mar 08 '18 at 09:04
  • why do you assume that the `==` chosen is `bool operator==(T const* a, T const* b)` and not `bool operator==(T * a, T * b)`? – Caleth Mar 08 '18 at 09:30
  • @MartinHennings Even if you needed to cast `testInst` into a const pointer, still `testInst` would be a non-const object. This is what matters when const/non-const member function/operator overloads are resolved. – Daniel Langr Mar 08 '18 at 09:36
  • @MartinHennings Look here for live example to (hopefully) understand what happens: https://wandbox.org/permlink/rAvIHMAhSeao5tUl – Daniel Langr Mar 08 '18 at 09:43

3 Answers3

5

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.

Community
  • 1
  • 1
Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
0

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

Martin Hennings
  • 16,418
  • 9
  • 48
  • 68
0

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.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93