拥有空对基类的目的是什么?

Pas*_* By 85 c++ libstdc++

libstdc++的 pair 实现有以下奇怪之处

template<typename, typename> class __pair_base
  {
    template<typename T, typename U> friend struct pair;
    __pair_base() = default;
    ~__pair_base() = default;
    __pair_base(const __pair_base&) = default;
    __pair_base& operator=(const __pair_base&) = delete;
  };

template<typename T, typename U>
  struct pair
  : private __pair_base<T, U>
{ /* never uses __pair_base */ };
Run Code Online (Sandbox Code Playgroud)

__pair_base从未使用过,也不能使用,因为它是空的。这是特别令人迷惑,因为std::pair是需要是有条件的结构

pair<T, U>如果T和U都是结构类型,则是结构类型。

拥有私人基地使其成为非结构性的。

Pas*_* By 108

tl;dr 这是为实现std::pairABI 兼容性的疯狂重载/显式规则而进行的一系列非常长的黑客攻击的结果。这是 C++20 中的一个错误。


免责声明

这更像是与标准库作者一起沿着记忆通道走的“有趣”之旅,然后是一些有见地的语言级别的启示。它显示了 C++ 变得多么复杂,实现pair是一项艰巨的任务。

我尽力重现历史,但我不是作者之一。

配对引物

std::pair 不仅仅是简单的

template<typename T, typename U>
struct pair
{
    T first;
    U second;
};
Run Code Online (Sandbox Code Playgroud)

cppreference 上列出了 8 个不同的构造函数,对于实现者来说,它甚至更多:每个条件显式构造函数实际上是两个构造函数,一个用于隐式,另一个用于显式。

并非所有这些构造函数都参与重载决议,如果他们参与了,就会到处都有歧义。取而代之的是,有许多规则来管理每个规则,并且 上述情况的每种组合都必须由 SFINAE 手动编写和禁用。

多年来,仅在构造函数上就产生了 5 个错误报告。现在即将成为 6 ;)

序幕

第一个错误是关于在类型相同的情况下短路对参数的可转换性检查。

template<typename T> struct B;
template<typename T> struct A
{
    A(A&&) = default;
    A(const B<T> &);
};

template<typename T> struct B
{
    pair<A<T>, int> a;
    B(B&&) = default;
};
Run Code Online (Sandbox Code Playgroud)

显然,如果他们过早地检查可转换性,则移动构造函数会由于循环依赖而被删除,并且 howB在A.

nonesuch

然而,这改变了SFINAE性质的pair。作为回应,实施了另一个修复程序。此实现启用了以前无效的赋值运算符,因此通过更改其签名手动关闭赋值运算符

struct nonesuch
{
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

// ...
pair& operator=(
    conditional_t<conjunction_v<is_copy_assignable<T>,
                                is_copy_assignable<U>>,
                  const pair&, const nonesuch&>::type)
Run Code Online (Sandbox Code Playgroud)

哪里nonesuch是一个虚拟类型,它基本上使这个重载无法调用。或者是吗?

no_braces_nonesuch

不幸的是,即使你永远无法创建一个 nonesuch

pair<int, int> p = {};  // succeeds
p = {};  // fails
Run Code Online (Sandbox Code Playgroud)

你仍然可以用大括号初始化它。由于delete不解决重载决议,这是一个硬失败。

解决方法是创建 no_braces_nonesuch

struct no_braces_nonesuch : nonesuch
{
    explicit no_braces_nonesuch(const no_braces_nonesuch&) = delete;
};
Run Code Online (Sandbox Code Playgroud)

该explicit关在重载参与圈。最后,分配是不可调用的。或者是……?

__pair_base v1

不幸的是,还有另一种初始化未知类型的方法

struct anything
{
    template<typename T>
    operator T() { return {}; }
};

anything a;
pair<int, int> p;
p = a;
Run Code Online (Sandbox Code Playgroud)

作者意识到他们可以通过利用默认生成的特殊成员函数“轻松”解决这个问题:如果您有一个不可分配的基类,则根本不会声明它们

class __pair_base
  {
    template<typename T, typename U> friend struct pair;
    __pair_base() = default;
    ~__pair_base() = default;
    __pair_base(const __pair_base&) = default;
    __pair_base& operator=(const __pair_base&) = delete;
  };
Run Code Online (Sandbox Code Playgroud)

所有单元测试都通过了,事情看起来很光明。不知不觉中,一只邪恶虫子的影子不祥地隐约出现在地平线上。

__pair_base v2

ABI 坏了。

这怎么可能呢?空碱基被优化掉了不是吗?嗯,不。

pair<pair<int, int>, int> p;
Run Code Online (Sandbox Code Playgroud)

不幸的是,空基优化仅适用于基类子对象与相同类型的其他子对象不重叠的情况。在这种情况下,__pair_base内部对的 与外部对中的一个重叠。

修复很“简单”,我们模板化__pair_base以确保它们是不同的类型。

结构类型

C++20 来了,它要求那对是结构类型。这要求没有私人基地。

template<pair<int, int>>
struct S;  // fails
Run Code Online (Sandbox Code Playgroud)

就这样结束了我们的旅程。这让我想起了Chandler Carruth 在 cppcon 上的快速调查:“如果需要,谁能在一年内构建一个 C++ 编译器?” 鉴于 C++ 的复杂程度,只有当前的编译器作者认为他们可以。显然,我什至不知道如何实现std::pair.

  • +1 的背景和演示。如果我即将在 Mbed OS 中尝试一个简单的应用程序并没有让我完全抓狂,我可能会问“C++ 是一个神秘的邪教吗?” 明年四月。 (9认同)
  • 回复最后一句话。再说一遍,“std::pair”是用 C++ 实现的,而不是编译器本身的一部分 (3认同)