来自空大括号的模糊复制赋值的编译器差异

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。

演示

我最初的假设是该程序:

  • 在 C++11 和 C++14 中格式错误,因为nullopt_t(如上)是一个聚合,而它
  • 是公形成式C ++ 17和C ++ 20,因为它不再是一种聚集体,这意味着它的显式默认的构造应该禁止的临时副本列表-INITnullopt_t根据需要用于在拷贝赋值运算符对象#2是可行的,

但是没有一个编译器完全同意这个理论,有些我可能遗漏了一些东西。

什么编译器在这里是正确的(如果有的话),我们如何通过相关的标准部分(和 D​​R:s,如果相关)来解释它?

dfr*_*fri 1

为什么nullopt_t一定要放在第一位DefaultConstructible呢?

\n

nullopt_t回想起来,不应该的规范要求DefaultConstructible可以说是一个基于标签类型的一些 LWG 和 CWG 混淆的错误,而这种混淆的解决是在从 Library Fundamentals TS Components 引入之后 std::optional才出现的。

\n

首先,当前的(C++17、C++20)规范nullopt_t[可选.nullopt]/2,需要[强调我的]:

\n
\n

类型nullopt_\xc2\xadt 不应具有默认构造函数或初始值设定项列表构造函数,并且不应是聚合。

\n
\n

其主要用途已在上一节中描述,[optional.nullopt]/1

\n
\n

[...] 特别是,optional<T>有一个构造函数nullopt_\xc2\xadt作为单个参数;这表明应构造一个不包含值的可选对象。

\n
\n

现在,P0032R3的同质接口variantanyoptional),作为介绍的一部分的论文之一,围绕、 一般标签类型和要求 [重点是我的 ]std::optional进行了讨论:nullopt_tDefaultConstructible

\n
\n

没有默认可构造

\n

在适应optional<T>in_place_t类型时,我们发现\n我们不能再使用in_place_t{}. 作者不认为这是一个很大的限制,因为用户可以使用in_place它。nullopt_t需要注意的是,这与as的行为一致,nullopt_t{}因为没有默认的可构造性而失败。然而nullptr_t{}\n似乎格式良好。

\n

不可分配自{}

\n

经过更深入的分析,我们还发现旧的in_place_t\n支持in_place_t t = {};. 作者不认为这是一个很大的限制,因为我们不希望很多用户可以使用它,而用户可以使用\nin_place代替。

\n
in_place_t t;\nt = in_place;\n
Run Code Online (Sandbox Code Playgroud)\n

需要注意的是,这与\n的行为一致nullopt_t,因为下面的编译失败。

\n
nullopt_t t = {}; // compile fails\n
Run Code Online (Sandbox Code Playgroud)\n

不过nullptr_t貌似还是支持一下。

\n
nullptr_t t = {}; // compile pass\n
Run Code Online (Sandbox Code Playgroud)\n

为了重新实施此设计,有一个悬而未决的问题 2510 - 标签类型不应是DefaultConstructible核心问题 2510。

\n
\n

事实上,LWG 核心问题 2510最初提出的解决方案要求所有标签类型都不是[强调我的]: DefaultConstructible

\n
\n

(LWG) 2510. 标签类型不应DefaultConstructible

\n

[...]

\n

先前的决议[已取代]:

\n

[...]在 20.2 [utility]/2 之后添加一个新段落(在标题概要之后):

\n
    \n
  • -?- 类型piecewise_construct_t 不应有默认构造函数。它应该是文字类型。常量piecewise_construct应使用文字类型的参数进行初始化。
  • \n
\n
\n

然而,该决议已被取代,因为与CWG 核心问题 1518存在重叠,该问题最终以不要求标签类型的方式得到解决DefaultConstructibleexplicit就足够了 [重点我的]:

\n
\n

(CWG) 1518. 显式默认构造函数和复制列表初始化

\n

[...]

\n

附加说明,2015 年 10 月:

\n

有人认为问题 1630 的解决方案在允许使用显式构造函数进行默认初始化方面走得太远了,并且应该将默认初始化视为模型复制初始化。这个问题的解决将为调整提供一个机会。

\n

拟议决议(2015 年 10 月):

\n

将 12.2.2.4 [over.match.ctor] 第 1 段更改如下:

\n

[...] 用于直接初始化或默认初始化,候选函数是被初始化对象的类的所有构造函数。[...]

\n
\n

只要explicit还暗示该类型不是聚合,这又是 LWG 核心问题 2510 的最终解决方案(基于 CWG 核心问题 1518 的最终解决方案)

\n
\n

(LWG) 2510. 标签类型不应DefaultConstructible

\n

[...]

\n

提议的决议:

\n

[...]在20.2 [utility]/2中,更改标题概要:

\n
    \n
  • \n
    // 20.3.5, pair piecewise construction\nstruct piecewise_construct_t { explicit piecewise_construct_t() = default; };\nconstexpr piecewise_construct_t piecewise_construct{};\n
    Run Code Online (Sandbox Code Playgroud)\n
  • \n
\n

[...]

\n
\n

然而,后面的这些变化没有被纳入提案中std::optional,这可以说是一种疏忽,我想声明nullopt_t不需要要求它DefaultConstructible像其他标签类型一样,只是应该有一个用户声明的构造explicit函数,这会禁止它成为空大括号 copy-list-init 的候选者,因为它不是聚合,而且唯一的候选构造函数是explicit

\n

这里哪个编译器是对的,哪个是错的?

\n

鉴于 LWG 2510、CWG 1518(和其他)的混乱,让我们重点关注 C++17 及更高版本。在这种情况下,GCC 拒绝该程序可以说是错误的,而 Clang 和 MSVC 接受它是正确的。

\n

为什么?

\n

因为S& operator=(nullopt_t)赋值运算符对于赋值是不可行的s = {};,因为空大括号{}需要聚合初始化或复制列表初始化来创建nullopt_t(临时)对象。nullopt_t,但是(通过惯用标记实现:我上面的实现),根据P0398R0(解决 CWG 核心问题 1518),既不是聚合,也不是其默认构造函数参与复制列表初始化(来自空大括号)。

\n

这可能属于以下 GCC 错误报告:

\n\n

列于SUSPENDED2015-06-15,在 CWG 核心问题 1630 决议发生变化之前(“问题 1630 的决议走得太远”)。现在根据此问答中的 ping 重新打开票证。

\n