Search code examples
c++constantsshared-ptrcomparison-operators

Shared pointer constness in comparison operator ==


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?

Solution

  • 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.