问题
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
- Is this because a function cannot be selected by the type of the return value alone?
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:
- Let's look at the types:
testInst
is of typeSharedPointer<int>
.nullptr
is (for the sake of simplification) of typeT*
orvoid*
, depending on the use case.
So the expression readsSharedPointer<int> == int*
. - We need to have equal types to invoke a comparison operator. There are two possibilities:
- Resolve to
int* == int*
.
This involves a call tooperator int *()
oroperator int const *() const
.
[citation needed] - Resolve to
SharedPointer<int> == SharedPointer<int>
This involves a call toSharedPointer(nullptr)
.
- Resolve to
- Because the second option would create a new object, and the first one doesn't, the first option is the better match. [citation needed]
- Now before resolving
bool operator==(int [const] *a, int [const] *b)
(the[const]
is irrelevant),testInst
must be converted toint*
.
This involves a call to the conversionoperator int *()
oroperator int const *() const
.
Here, the non-const version will be called becausetestInst
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