dfr*_*fri 6 c++ gcc clang language-lawyer compiler-bug
我一直在试图理解std::nullopt_t
不允许DefaultConstructible
在 C++17(引入它的地方)及更高版本中的基本原理,并在此过程中解决了一些编译器差异混淆。
考虑以下违反规范的(它是DefaultConstructible
)实现nullopt_t
:
struct nullopt_t {
explicit constexpr nullopt_t() = default;
};
Run Code Online (Sandbox Code Playgroud)
它是 C++11 和 C++14(无用户提供的构造函数)中的聚合,但不是 C++17(构造函数explicit
)和 C++20(用户声明的构造函数)中的聚合。
现在考虑以下示例:
struct S {
constexpr S() {}
S(S const&) {}
S& operator=(S const&) { return *this; } // #1
S& operator=(nullopt_t) { return *this; } // #2
};
int main() {
S s{};
s = {}; // GCC error: ambiguous overload for 'operator=' (#1 and #2)
}
Run Code Online (Sandbox Code Playgroud)
这在 C++11 到 C++20 中被 GCC(各种版本,比如 v11.0)拒绝,但在 C++11 到 C++11 中被 Clang(比如 v12.0)和 MSVC(v19.28)接受C++20。
我最初的假设是该程序:
nullopt_t
(如上)是一个聚合,而它nullopt_t
根据需要用于在拷贝赋值运算符对象#2
是可行的,但是没有一个编译器完全同意这个理论,有些我可能遗漏了一些东西。
什么编译器在这里是正确的(如果有的话),我们如何通过相关的标准部分(和 DR:s,如果相关)来解释它?
nullopt_t
一定要放在第一位DefaultConstructible
呢?nullopt_t
回想起来,不应该的规范要求DefaultConstructible
可以说是一个基于标签类型的一些 LWG 和 CWG 混淆的错误,而这种混淆的解决是在从 Library Fundamentals TS Components 引入之后 std::optional
才出现的。
首先,当前的(C++17、C++20)规范nullopt_t
,[可选.nullopt]/2,需要[强调我的]:
\n\n类型
\nnullopt_\xc2\xadt
不应具有默认构造函数或初始值设定项列表构造函数,并且不应是聚合。
其主要用途已在上一节中描述,[optional.nullopt]/1:
\n\n\n[...] 特别是,
\noptional<T>
有一个构造函数nullopt_\xc2\xadt
作为单个参数;这表明应构造一个不包含值的可选对象。
现在,P0032R3(、和的同质接口variant
any
optional
),作为介绍的一部分的论文之一,围绕、 一般标签类型和要求 [重点是我的 ]std::optional
进行了讨论:nullopt_t
DefaultConstructible
\n\n没有默认可构造
\n在适应
\noptional<T>
新in_place_t
类型时,我们发现\n我们不能再使用in_place_t{}
. 作者不认为这是一个很大的限制,因为用户可以使用in_place
它。nullopt_t
需要注意的是,这与as的行为一致,nullopt_t{}
因为没有默认的可构造性而失败。然而nullptr_t{}
\n似乎格式良好。不可分配自
\n{}
经过更深入的分析,我们还发现旧的
\nin_place_t
\n支持in_place_t t = {};
. 作者不认为这是一个很大的限制,因为我们不希望很多用户可以使用它,而用户可以使用\nin_place
代替。Run Code Online (Sandbox Code Playgroud)\nin_place_t t;\nt = in_place;\n
需要注意的是,这与\n的行为一致
\nnullopt_t
,因为下面的编译失败。Run Code Online (Sandbox Code Playgroud)\nnullopt_t t = {}; // compile fails\n
不过
\nnullptr_t
貌似还是支持一下。Run Code Online (Sandbox Code Playgroud)\nnullptr_t t = {}; // compile pass\n
为了重新实施此设计,有一个悬而未决的问题 2510 - 标签类型不应是
\nDefaultConstructible
核心问题 2510。
事实上,LWG 核心问题 2510最初提出的解决方案是要求所有标签类型都不是[强调我的]: DefaultConstructible
\n\n(LWG) 2510. 标签类型不应
\nDefaultConstructible
[...]
\n先前的决议[已取代]:
\n[...]在 20.2 [utility]/2 之后添加一个新段落(在标题概要之后):
\n\n
\n- -?- 类型
\npiecewise_construct_t
不应有默认构造函数。它应该是文字类型。常量piecewise_construct
应使用文字类型的参数进行初始化。
然而,该决议已被取代,因为与CWG 核心问题 1518存在重叠,该问题最终以不要求标签类型的方式得到解决DefaultConstructible
,explicit
就足够了 [重点我的]:
\n\n(CWG) 1518. 显式默认构造函数和复制列表初始化
\n[...]
\n附加说明,2015 年 10 月:
\n有人认为问题 1630 的解决方案在允许使用显式构造函数进行默认初始化方面走得太远了,并且应该将默认初始化视为模型复制初始化。这个问题的解决将为调整提供一个机会。
\n拟议决议(2015 年 10 月):
\n将 12.2.2.4 [over.match.ctor] 第 1 段更改如下:
\n[...] 用于直接初始化
\n或默认初始化,候选函数是被初始化对象的类的所有构造函数。[...]
只要explicit
还暗示该类型不是聚合,这又是 LWG 核心问题 2510 的最终解决方案(基于 CWG 核心问题 1518 的最终解决方案)
\n\n(LWG) 2510. 标签类型不应
\nDefaultConstructible
[...]
\n提议的决议:
\n[...]在20.2 [utility]/2中,更改标题概要:
\n\n
\n- \n
\nRun Code Online (Sandbox Code Playgroud)\n// 20.3.5, pair piecewise construction\nstruct piecewise_construct_t { explicit piecewise_construct_t() = default; };\nconstexpr piecewise_construct_t piecewise_construct{};\n
[...]
\n
然而,后面的这些变化没有被纳入提案中std::optional
,这可以说是一种疏忽,我想声明nullopt_t
不需要要求它DefaultConstructible
像其他标签类型一样,只是应该有一个用户声明的构造explicit
函数,这会禁止它成为空大括号 copy-list-init 的候选者,因为它不是聚合,而且唯一的候选构造函数是explicit
。
鉴于 LWG 2510、CWG 1518(和其他)的混乱,让我们重点关注 C++17 及更高版本。在这种情况下,GCC 拒绝该程序可以说是错误的,而 Clang 和 MSVC 接受它是正确的。
\n为什么?
\n因为S& operator=(nullopt_t)
赋值运算符对于赋值是不可行的s = {};
,因为空大括号{}
需要聚合初始化或复制列表初始化来创建nullopt_t
(临时)对象。nullopt_t
,但是(通过惯用标记实现:我上面的实现),根据P0398R0(解决 CWG 核心问题 1518),既不是聚合,也不是其默认构造函数参与复制列表初始化(来自空大括号)。
这可能属于以下 GCC 错误报告:
\n\n列于SUSPENDED
2015-06-15,在 CWG 核心问题 1630 决议发生变化之前(“问题 1630 的决议走得太远”)。现在根据此问答中的 ping 重新打开票证。