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

标签 c++ gcc clang language-lawyer compiler-bug

我一直试图理解 std::nullopt_t 的基本原理不允许是DefaultConstructible在 C++17(它被引入的地方)及更高版本中,并在此过程中克服了一些编译器差异混淆。
考虑以下违反规范的(它是 DefaultConstructible )实现 nullopt_t :

struct nullopt_t {
    explicit constexpr nullopt_t() = default;
};
这是 C++11 和 C++14(无用户提供的 ctor)中的聚合,但不是 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++11 到 C++ 中被 Clang(比如 v12.0)和 MSVC(v19.28)接受C++20。
DEMO
我最初的假设是该程序:
  • 在 C++11 和 C++14 中格式不正确,如 nullopt_t (如上)是一个聚合,而它
  • 在 C++17 和 C++20 中格式正确,因为它不再是一个聚合,这意味着它的显式默认构造函数应该禁止临时 nullopt_t 的复制列表初始化#2 处的复制赋值运算符所需的对象可行,

  • 但是没有一个编译器完全同意这个理论,有些我可能遗漏了一些东西。
    什么编译器在这里是正确的(如果有的话),我们如何通过相关的标准部分(和 D​​R:s,如果相关)来解释它?

    最佳答案

    为什么是 nullopt_t必须是 DefaultConstructible首先?nullopt_t 的规范要求不得为 DefaultConstructible回想起来,可以说是一个基于标签类型的一些 LWG 和 CWG 混淆的错误,并且这种混淆的解决仅在 std::optional 之后才出现。是 brought in from the Library Fundamentals TS Components .
    首先,nullopt_t 的当前 (C++17, C++20) 规范, [optional.nullopt]/2 , 需要 [ 重点矿]:

    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.


    现在,P0032R3 (variantanyoptional 的同构界面),其中一篇论文是介绍 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不必是DefaultConstructible , 只是,与其他标签类型一样,它应该有一个用户声明的 explicit构造函数,它禁止它作为空括号复制列表初始化的候选对象,因为它不是一个聚合,并且唯一的候选构造函数是 explicit .
    哪个编译器在这里是对还是错?
    鉴于 LWG 2510、CWG 1518(和其他)混淆,让我们关注 C++17 及更高版本。在这种情况下,GCC 拒绝该程序可以说是错误的,而 Clang 和 MSVC 接受它是正确的。
    为什么?
    因为S& operator=(nullopt_t)赋值运算符不适用于赋值 s = {}; , 作为空括号 {}将需要聚合初始化或复制列表初始化来创建 nullopt_t (临时)对象。 nullopt_t ,但是(通过惯用的标签实现:我上面的实现),根据 P0398R0 (解决了 CWG 核心问题 1518),既不是聚合,也不是其默认构造函数参与复制列表初始化(来自空括号)。
    这可能属于以下 GCC 错误报告:
  • Bug 54835 - (C++11)(DR 1518) Explicit default constructors not respected during copy-list-initialization

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

    关于c++ - 来自空括号的不明确复制分配的编译器差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67469039/

    相关文章:

    c++ - 在 GCC 的内联汇编中使用 C++ 引用

    linux - 在 Debian 上链接依赖于位置的程序集

    c++ - 在 C++ 中保存和加载存储在二进制文件中的类数据

    c++ - 强制编译器生成整个类模板

    c++ - SQLite 注册 VFS 导致段错误

    c++ - 可移植的 c++ 对齐?

    ruby - 使用 Xcode 4.3 在 OS X Lion 上安装 Ruby <1.9.3 的正确方法是什么?

    c - 如何重新定义#defined 值?

    c++ - "Static member function overrides a virtual function in a base class"被 gcc 和 clang 捕获,但未被 VC++ 捕获

    c++ - 为什么 clang 编译器标志的顺序会影响生成的二进制大小?