来自空大括号的模糊复制赋值的编译器差异 为什么首先需要 nullopt_t 为 DefaultConstructible?无默认构造不能从 nullptr_t{} 赋值(LWG) 2510标记类型不应为 DefaultConstructible(CWG) 1518. 显式默认构造函数和复制列表初始化(LWG) 2510标记类型不应为 explicit这里哪个编译器是对的,哪个是错的?

问题描述

我一直试图理解在 C++17(它被引入的地方)及更高版本中不允许 std::nullopt_t 成为 DefaultConstructible 的基本原理,并克服了一些编译器差异混淆过程中。

考虑以下违反规范(它是 DefaultConstructible)的 nullopt_t 实现:

struct nullopt_t {
    explicit constexpr nullopt_t() = default;
};

它是 C++11 和 C++14 中的聚合(没有用户提供ctors),但它不是 C++17 中的聚合(explicit ctor ) 和 C++20(用户声明 ctor)。

现在考虑以下示例:

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

这在 C++11 到 C++20 中被 GCC(各种版本,比如 v11.0)拒绝,但在 C+ 中被 Clang(比如 v12.0)和 MSVC(v19.28)接受+11 到 C++20。

DEMO

我最初的假设是该程序:

  • 在 C++11 和 C++14 中格式错误,因为 nullopt_t(如上所述)是一个聚合,而它
  • 在 C++17 和 C++20 中格式良好,因为它不再是一个聚合,这意味着它的显式认构造函数应该禁止临时 { {1}} 对象,使 nullopt_t 处的复制赋值运算符可行,

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

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

解决方法

为什么首先需要 nullopt_tDefaultConstructible

nullopt_t 不应为 DefaultConstructible 的规范要求,回想起来,可以说是基于围绕标签类型的一些 LWG 和 CWG 混淆的错误,而这种混淆的解决方案只是after std::optionalbrought in from the Library Fundamentals TS Components

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

类型 nullopt_­t 不应有默认构造函数或初始化列表构造函数,并且不应是聚合。

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

[...] 特别是,optional<T> 有一个构造函数,其中 nullopt_­t 作为单个参数;这表示将构造一个不包含值的可选对象。

现在,P0032R3variantanyoptional 的同构接口)是介绍{的一部分的论文之一{1}},讨论了 std::optional、一般标记类型和 nullopt_t 要求 [重点 我的]:

无默认构造

在使 DefaultConstructible 适应新的 optional<T> 类型时,我们发现 我们不能再使用 in_place_t。作者不考虑 这是一个很大的限制,因为用户可以使用 in_place_t{} 代替。它需要 需要注意的是,这与 in_place 的行为一致 nullopt_t 失败,因为没有默认构造。然而nullopt_t{} 看起来结构很好。

不能从 nullptr_t{} 赋值

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

in_place

需要说明的是,这符合 in_place_t t; t = in_place; 因为以下编译失败。

nullopt_t

不过 nullopt_t t = {}; // compile fails 似乎支持它。

nullptr_t

为了重新执行此设计,有一个待处理的问题 2510 - 标签类型不应是 nullptr_t t = {}; // compile pass 核心问题 2510。

实际上,LWG Core Issue 2510 最初提议的解决方案是要求所有标签类型不得DefaultConstructible [强调 我的]:

(LWG) 2510。标记类型不应为 DefaultConstructible

[...]

以前的解决方案[已取代]:

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

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

但是,由于与 CWG Core Issue 1518 重叠,因此该解决方案已被取代,最终以不要求标签类型不为 piecewise_construct 的方式解决,因为 DefaultConstructible足够[强调我的]:

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

[...]

附加说明,2015 年 10 月:

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

提议的决议(2015 年 10 月):

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

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

只要 explicit 还暗示类型不是聚合,而聚合又是 LWG Core Issue 2510 的最终解决方案(基于 CWG Core Issue 1518 的最终解决方案)

(LWG) 2510。标记类型不应为 explicit

[...]

提议的解决方案:

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

  • DefaultConstructible

[...]

然而,后面的这些变化没有被纳入 // 20.3.5,pair piecewise construction struct piecewise_construct_t { explicit piecewise_construct_t() = default; }; constexpr piecewise_construct_t piecewise_construct{}; 的提案中,可以说是一种疏忽,我想声明 std::optional 不需要被要求不是 nullopt_t ,只是,像其他标签类型一样,它应该有一个用户声明的 DefaultConstructible 构造函数,它禁止它作为空大括号 copy-list-init 的候选者,因为它既不是聚合又是唯一的候选者构造函数是 explicit

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

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

为什么?

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

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

在 2015 年 6 月 15 日被列为 nullopt_t,在 CWG 核心问题 1630 的解决方案发生变化之前(“问题 1630 的解决方案走得太远了”)。现在根据此问答的 ping 重新打开票证。