来自空括号的不明确复制分配的编译器差异

Compiler variance for ambiguous copy-assignment from empty-braces

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

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

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

这是 C++11 和 C++14 中的聚合(没有用户提供 构造器),但不是 C++17 中的聚合(explicit ctor) 和 C++20 (user-declared 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)拒绝,但在整个过程中被 Clang(比如 v12.0)和 MSVC(v19.28)接受C++11 到 C++20。

DEMO

我最初的假设是程序:

但是 none 的编译器完全同意这个理论,有些我可能遗漏了什么。

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

为什么 nullopt_t 首先必须是 DefaultConstructible

nullopt_t 不应 DefaultConstructible 的规范要求,回想起来,可以说是一个基于 LWG 和 CWG 围绕标签类型混淆的错误,而这种混淆的解决仅 std::optional 之后是 brought in from the Library Fundamentals TS Components

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

Type nullopt_­t shall not have a default constructor or an initializer-list constructor, and shall not be an aggregate.

其主要用途在上一节中有介绍,[optional.nullopt]/1:

[...] In particular, optional<T> has a constructor with nullopt_­t as a single argument; this indicates that an optional object not containing a value shall be constructed.

现在,P0032R3variantanyoptional 的同构接口,其中一篇论文是介绍 std::optional 的一部分,围绕 nullopt_t、一般标记类型和 DefaultConstructible 要求进行了讨论 [强调 我的]:

No default constructible

While adapting optional<T> to the new in_place_t type we found that we cannot anymore use in_place_t{}. The authors don't consider this a big limitation as the user can use in_place instead. It needs to be noted that this is in line with the behavior of nullopt_t as nullopt_t{} fails as no default constructible. However nullptr_t{} seems to be well formed.

Not assignable from {}

After a deeper analysis we found also that the old in_place_t supported in_place_t t = {};. The authors don't consider this a big limitation as we don't expect that a lot of users could use this and the user can use in_place instead.

in_place_t t;
t = in_place;

It needs to be noted that this is in line with the behavior of nullopt_t as the following compile fails.

nullopt_t t = {}; // compile fails

However nullptr_t seems to be support it.

nullptr_t t = {}; // compile pass

To re-enforce this design, there is an pending issue 2510-Tag types should not be DefaultConstructible Core issue 2510.

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

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Previous resolution [SUPERSEDED]:

[...] Add a new paragraph after 20.2 [utility]/2 (following the header synopsis):

  • -?- Type piecewise_construct_t shall not have a default constructor. It shall be a literal type. Constant piecewise_construct shall be initialized with an argument of literal type.

但是,由于与 CWG Core Issue 1518 有重叠,此决议已被取代,最终以不要求标签类型不是 DefaultConstructible 的方式得到解决,如 explicit 就够了 [强调 我的]:

(CWG) 1518. Explicit default constructors and copy-list-initialization

[...]

Additional note, October, 2015:

It has been suggested that the resolution of issue 1630 went too far in allowing use of explicit constructors for default initialization, and that default initialization should be considered to model copy initialization instead. The resolution of this issue would provide an opportunity to adjust that.

Proposed resolution (October, 2015):

Change 12.2.2.4 [over.match.ctor] paragraph 1 as follows:

[...] For direct-initialization or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. [...]

只要explicit也暗示该类型不是聚合,这又是 LWG Core Issue 2510 的最终决议(基于 CWG Core Issue 1518 的最终决议)

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Proposed resolution:

[...] In 20.2 [utility]/2, change the header synopsis:

  • // 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 不需要不是 [=15] =], 只是,像其他标签类型一样,它应该有一个用户声明的 explicit 构造函数,它禁止它成为空括号复制列表初始化的候选者,因为它不是聚合,而且唯一的候选构造函数是 explicit.

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

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

为什么?

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

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

在 2015 年 6 月 15 日被列为 SUSPENDED,在 CWG 核心问题 1630 的决议更改之前(“问题 1630 的决议走得太远”).现在根据此问答中的 ping 重新打开工单。