空对碱基 class 的目的是什么?
What is the purpose of having an empty pair base class?
libstdc++ 对的实现有以下奇怪之处
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 */ };
__pair_base
从未使用过,考虑到它是空的,也不能使用。这特别令人困惑,因为 std::pair
是 required 有条件地结构化
pair<T, U>
is a structural type if T
and U
are both structural types.
拥有私人基地使其变得非结构化。
tl;dr 这是为了实施 std::pair
的疯狂 overload/explicit 规则并保持 ABI 兼容性而进行的一系列黑客攻击的结果。这是 C++20 中的错误。
免责声明
这更像是一次“有趣”的旅程,与标准库作者一起沿着记忆之路前进,然后是一些有见地的语言水平的启示。它显示了 C++ 变得多么复杂,实现 对 是一项艰巨的任务。
我尽力重现历史,但我不是作者之一
配对入门
std::pair
不仅仅是
template<typename T, typename U>
struct pair
{
T first;
U second;
};
cppreference 上列出了 8 个不同的构造函数,对于实现者来说,甚至更多:每个条件显式构造函数实际上是 两个 个构造函数,一个用于隐式,另一个用于显式。
并非所有这些构造函数都参与了重载决议,如果他们参与了,到处都会有歧义。取而代之的是,有很多规则来管理每个事件的发生时间,每个 上述情况的组合都必须由 SFINAE 手动编写和禁用。
多年来,仅构造函数就产生了 5 个错误报告。现在快变成 6 了 ;)
序言
first bug是关于short-circuiting如果类型相同,对参数对的可转换性的检查。
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;
};
显然,如果他们过早检查可转换性,移动构造函数会由于循环依赖而被删除,并且 B
如何在 A
中仍然不完整。
nonesuch
这可是pair
的changed the SFINAE properties。作为回应,实施了另一个修复。此实现启用了以前无效的赋值运算符,因此通过更改其签名手动关闭了赋值运算符
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)
其中 nonesuch
是一个虚拟类型,它本质上使这个重载不可调用。或者是吗?
no_braces_nonesuch
不幸的是,即使您无法创建 nonesuch
pair<int, int> p = {}; // succeeds
p = {}; // fails
你仍然可以 initialize it with braces。由于 delete
不解决重载决议,这是一个硬故障。
解决方法是创建 no_braces_nonesuch
struct no_braces_nonesuch : nonesuch
{
explicit no_braces_nonesuch(const no_braces_nonesuch&) = delete;
};
explicit
关闭参与重载决议。最后,任务是不可调用的。还是……?
__pair_base
v1
不幸的是,another way to initialize 未知类型
struct anything
{
template<typename T>
operator T() { return {}; }
};
anything a;
pair<int, int> p;
p = a;
作者意识到他们可以通过利用默认生成的特殊成员函数“轻松地”解决这个问题:如果你有一个基数 non-assignable
,则根本可以不声明它们
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;
};
所有单元测试都通过了,一切看起来都很顺利。不为人知的是,一只邪恶虫子的影子不祥地笼罩在 horizon.
__pair_base
v2
ABI 坏了。
这怎么可能呢? Empty bases are optimized 出来了不是吗?嗯,没有。
pair<pair<int, int>, int> p;
不幸的是,只有当基础 class 子对象与其他相同类型的子对象 non-overlapping 时,空基础优化才适用。在这种情况下,内部对的 __pair_base
与外部对的一个重叠。
修复很“简单”,我们模板化 __pair_base
以确保它们是不同的类型。
结构类型
C++20 来了,它要求对 structural types。这就要求没有私人基地。
template<pair<int, int>>
struct S; // fails
我们的旅程就这样结束了。这让我想起 Chandler Carruth's quick survey at cppcon:“如果需要,谁能在一年内构建一个 C++ 编译器?”考虑到 C++ 的复杂性,只有当前的编译器编写者认为他们可以。显然,我什至不知道如何实施 std::pair
.
libstdc++ 对的实现有以下奇怪之处
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 */ };
__pair_base
从未使用过,考虑到它是空的,也不能使用。这特别令人困惑,因为 std::pair
是 required 有条件地结构化
pair<T, U>
is a structural type ifT
andU
are both structural types.
拥有私人基地使其变得非结构化。
tl;dr 这是为了实施 std::pair
的疯狂 overload/explicit 规则并保持 ABI 兼容性而进行的一系列黑客攻击的结果。这是 C++20 中的错误。
免责声明
这更像是一次“有趣”的旅程,与标准库作者一起沿着记忆之路前进,然后是一些有见地的语言水平的启示。它显示了 C++ 变得多么复杂,实现 对 是一项艰巨的任务。
我尽力重现历史,但我不是作者之一
配对入门
std::pair
不仅仅是
template<typename T, typename U>
struct pair
{
T first;
U second;
};
cppreference 上列出了 8 个不同的构造函数,对于实现者来说,甚至更多:每个条件显式构造函数实际上是 两个 个构造函数,一个用于隐式,另一个用于显式。
并非所有这些构造函数都参与了重载决议,如果他们参与了,到处都会有歧义。取而代之的是,有很多规则来管理每个事件的发生时间,每个 上述情况的组合都必须由 SFINAE 手动编写和禁用。
多年来,仅构造函数就产生了 5 个错误报告。现在快变成 6 了 ;)
序言
first bug是关于short-circuiting如果类型相同,对参数对的可转换性的检查。
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;
};
显然,如果他们过早检查可转换性,移动构造函数会由于循环依赖而被删除,并且 B
如何在 A
中仍然不完整。
nonesuch
这可是pair
的changed the SFINAE properties。作为回应,实施了另一个修复。此实现启用了以前无效的赋值运算符,因此通过更改其签名手动关闭了赋值运算符
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)
其中 nonesuch
是一个虚拟类型,它本质上使这个重载不可调用。或者是吗?
no_braces_nonesuch
不幸的是,即使您无法创建 nonesuch
pair<int, int> p = {}; // succeeds
p = {}; // fails
你仍然可以 initialize it with braces。由于 delete
不解决重载决议,这是一个硬故障。
解决方法是创建 no_braces_nonesuch
struct no_braces_nonesuch : nonesuch
{
explicit no_braces_nonesuch(const no_braces_nonesuch&) = delete;
};
explicit
关闭参与重载决议。最后,任务是不可调用的。还是……?
__pair_base
v1
不幸的是,another way to initialize 未知类型
struct anything
{
template<typename T>
operator T() { return {}; }
};
anything a;
pair<int, int> p;
p = a;
作者意识到他们可以通过利用默认生成的特殊成员函数“轻松地”解决这个问题:如果你有一个基数 non-assignable
,则根本可以不声明它们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;
};
所有单元测试都通过了,一切看起来都很顺利。不为人知的是,一只邪恶虫子的影子不祥地笼罩在 horizon.
__pair_base
v2
ABI 坏了。
这怎么可能呢? Empty bases are optimized 出来了不是吗?嗯,没有。
pair<pair<int, int>, int> p;
不幸的是,只有当基础 class 子对象与其他相同类型的子对象 non-overlapping 时,空基础优化才适用。在这种情况下,内部对的 __pair_base
与外部对的一个重叠。
修复很“简单”,我们模板化 __pair_base
以确保它们是不同的类型。
结构类型
C++20 来了,它要求对 structural types。这就要求没有私人基地。
template<pair<int, int>>
struct S; // fails
我们的旅程就这样结束了。这让我想起 Chandler Carruth's quick survey at cppcon:“如果需要,谁能在一年内构建一个 C++ 编译器?”考虑到 C++ 的复杂性,只有当前的编译器编写者认为他们可以。显然,我什至不知道如何实施 std::pair
.