比较运算符中的共享指针常量 ==

Shared pointer constness in comparison operator ==

我偶然发现了我正在使用的共享指针的意外行为。

共享指针实现引用计数和分离(例如制作副本),如果需要的话,包含在非常量用法中的实例。
为此,对于每个 getter 函数,智能指针都有一个 const 和一个 non-const 版本,例如:operator T *()operator T const *() const.

问题: 将指针值与 nullptr 进行比较会导致分离。

预期:我以为比较运算符总是会调用 const 版本。

简化示例:
(此实现没有引用计数,但仍然显示问题)

#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
}

问题 1:为什么在 应该 足以调用 const 版本时调用非 const 版本的函数?

问题 2: 为什么 可以 调用非 const 版本?比较运算符(尤其是与不可变 nullptr 进行比较)是否总是对 const 引用进行操作?


备案:
我使用的共享指针是 Qt 的 QSharedDataPointer 持有一个 QSharedData 派生的实例,但这个问题不是特定于 Qt 的。


编辑:

据我了解,nullptr == testInst 会调用

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

(因为我为什么要比较非常量指针?)

哪个应该调用:

inline operator const T *() const 

更多问题:

所以这个问题归结为:

当 const 和 non-const 存在重载时,如果您使用的对象是 non-const,编译器将始终调用 non-const 版本。否则,什么时候调用 non-const 版本 ever

如果您想明确使用 const 版本,请通过 const 引用调用它们:

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

在 Qt 的 QSharedDataPointer 上下文中,您还可以在需要指针时显式使用 constData 函数。

对于 QSharedDataPointer 的预期用途,此行为通常不是问题。它意味着成为外观 class 的成员,因此只能从其成员函数中使用。那些不需要修改(因此不需要分离)的成员函数本身应该是 const,使成员对指针的访问处于 const 上下文中,因此不会分离。

编辑以回答编辑:

In my understanding, nullptr == testInst would invoke

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

这种理解是不正确的。运算符的重载解析相当复杂,参与解析的运算符的 built-in 版本有大量代理签名。这个过程在标准的 [over.match.oper] 和 [over.built] 中有描述。

具体来说,相关的 built-in 相等候选项在 [over.built]p16 和 17 中定义。这些规则表明对于 每个 指针类型 T,存在 operator ==(T, T)。现在,int*const int* 都是指针类型,所以两个相关的签名是 operator ==(int*, int*)operator ==(const int*, const int*)。 (还有operator ==(std::nullptr_t, std::nullptr_t),但不会被选中。)

为了区分这两个重载,编译器必须比较转换序列。对于第一个参数,nullptr_t -> int*nullptr_t -> const int* 都是相同的;它们是指针转换。将 const 添加到其中一个指针中。 (参见 [conv.ptr]。)对于第二个参数,转换分别为 SharedPointer<int> -> int*SharedPointer<int> -> const int*。其中第一个是 user-defined 转换,调用 operator int*(),无需进一步转换。第二个是 user-defined 转换,调用 operator const int*() const,这需要先进行资格转换才能调用 const 版本。因此,首选non-const版本。

这是因为表达式 testInst == nullptr 的解析方式:

  1. 让我们看看类型:
    testInst 属于 SharedPointer<int>.
    类型 nullptr 是(为了简化)类型 T*void*,具体取决于用例。
    所以表达式显示为 SharedPointer<int> == int*.
  2. 我们需要有相同的类型来调用比较运算符。有两种可能性:
    1. 解析为int* == int*
      这涉及调用 operator int *()operator int const *() const.
      [需要引用]
    2. 解析为SharedPointer<int> == SharedPointer<int>
      这涉及调用 SharedPointer(nullptr).
  3. 因为第二个选项会创建一个新对象,而第一个选项不会,所以第一个选项更匹配。 [需要引用]
  4. 现在解析bool operator==(int [const] *a, int [const] *b)之前([const]是无关紧要的),testInst必须转换为int*
    这涉及调用转换 operator int *()operator int const *() const.
    在这里,non-const 版本 将被调用,因为 testInst 不是 const。 [需要引用]

我在 Qt Bugs 上创建了一个建议,将 T* 的比较运算符添加到 QSharedDataPointer<T>https://bugreports.qt.io/browse/QTBUG-66946

也许这段代码可以让您了解发生了什么:

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;
}

输出为

1
2
3
4

如果转换 const 对象(或对它的引用),则选择 const 转换运算符(情况 3 和 4),反之亦然。