为啥在已删除的默认 ctor 旁边定义一个空的副本 ctor 会使空列表的值初始化失败?

Posted

技术标签:

【中文标题】为啥在已删除的默认 ctor 旁边定义一个空的副本 ctor 会使空列表的值初始化失败?【英文标题】:Why does defining an empty copy ctor beside a deleted default ctor make a value initialization with empty list fail?为什么在已删除的默认 ctor 旁边定义一个空的副本 ctor 会使空列表的值初始化失败? 【发布时间】:2021-04-18 04:50:18 【问题描述】:

简而言之,为什么下面的代码表现得像 cmets 中描述的那样?

struct A

    A() = delete;
    //A(const A&)  // uncommenting this...
;

int main()

    A a; // ... breaks this
    //A(); // this fails in either case because of `A() = delete;` only

我应该查看标准的哪一部分(或至少 cppreference 上的一个页面)来理解这一点?

但是,写A(const A&) = default; 而不是//A(const A&) 不会破坏A a;。那这个呢?我认为根本原因是一样的,但是真正懂C++的人说的比我认为要好。

【问题讨论】:

【参考方案1】:

没有user-provided 复制构造函数,A 是一个聚合。是的,即使我们删除了默认构造函数。那是addressed in C++20。

因此,在 C++20 之前,A a; 是 aggregate initialization,因此不使用已删除的构造函数。当您取消注释复制构造函数时,A 将不再是聚合,从而将聚合初始化转换为 value initialization。所以a的初始化会尝试调用被删除的构造函数。

为了从标准中判断初始化器的含义,通常从[dcl.init]/16 开始。通过项目符号,可以发现初始化程序的属性(当与相关类型的属性匹配时)将如何影响初始化发生的方式。

【讨论】:

我认为仅此一项就足以证明 C++ 只对专家友好的声誉。 我认为您的这条评论可以作为您回答的标题(我挑战您这样做!啊哈:P)。 @Enlico - 我不能说我完全喜欢这样移动球门柱。但无论哪种方式,这是完全相同的事情。它不再是用户提供的复制构造函数,因此不会干扰A 的聚合状态。 @Swift-FridayPie,实际上没有。我首先误读了您的评论(我读了没有用户定义的构造函数),我不明白,并想“如果我默认构造复制ctor会发生什么?”。 @Enlico 是同一枚硬币的两侧,两端都有尾巴。 A() = delete; A(const A&) = delete; A() = delete; A(const A&) = default; 都可以在 C++17 中编译,而不能在 C++20 中编译。【参考方案2】:

聚合初始化不是值初始化

如果给定的类,比如S,是一个聚合,那么

S s;

是聚合初始化,而不是值初始化,并且会绕过S 的任何构造函数,即使它们被删除或私有。聚合初始化的结果通常是聚合的 (public(1)) 数据成员的值初始化。

(1) 如下所示,具有任何私有数据成员的类绝不是聚合类,不符合任何 C++11 到 C++20 标准。


什么是聚合类的定义受以下因素支配:

C++11 中的[dcl.init.aggr]/1 (N3337) C++14 中的[dcl.init.aggr]/1 (N4140) C++17 中的[dcl.init.aggr]/1 (N4659) C++20 中的[dcl.init.aggr]/1 (N4861)

与聚合的典型混淆是 C++11 到 C++17 的较弱要求是没有任何 用户提供的(2) 构造函数,这在 C++20 中变得更加严格,要求没有任何用户声明的构造函数。

(2) 用户提供函数的定义由 C++11 中的 [dcl.fct.def.default]/4 和 C++14 到 C++20 中的 [dcl.fct.def.default]/5 管理; user-provided 的定义在所有这些语言版本中基本相同。

C++11、C++14 和 C++17 之间的聚合定义还有其他一些不为人知的变化,但同样增加了 C++20 之前的聚合混乱;我们将通过几个示例来突出它们。


示例是聚合吗?

对于下面的每个示例;如果示例的类S 是一个聚合(针对特定语言版本),则以下格式正确:

S s;

上述所有示例中的聚合规则同样适用于任何类型的构造函数(假设它们在其他方面适用,例如允许显式默认或删除),而不仅仅是S()


struct S ;
C++11:是的 C++14:是的 C++17:是的 C++20:是的
struct S 
    int x;
;
C++11:是的 C++14:是的 C++17:是的 C++20:是的
struct S 
    int x42;
;
C++11:否(具有默认成员初始化程序) C++14:是的 C++17:是的 C++20:是的
class S 
    int x;
;
C++11:否 C++14:否 C++17:否 C++20:否

有一个私有数据成员。


struct S 
    S() = default;
;
C++11:是的 C++14:是的 C++17:是的 C++20:否(具有用户声明的构造函数)
struct S 
 private:
    S() = default;
;
C++11:是的 C++14:是的 C++17:是的 C++20:否(具有用户声明的构造函数)
struct S 
    S() = delete;
;
C++11:是的 C++14:是的 C++17:是的 C++20:否(具有用户声明的构造函数)
struct S 
    S();
;
S::S() = default;
C++11:否(具有用户提供的构造函数) C++14:否 (...) C++17:否 (...) C++20:否(具有用户声明的构造函数)

显式默认定义不符合规定,视为用户提供;回顾 [dcl.fct.def.default]/4 (C++11) / [dcl.fct.def.default]/5 (C++14, C++17):

如果函数是用户声明的并且在其第一次声明时没有显式默认或删除,则该函数是用户提供的。


struct S 
    explicit S() = default;
;
C++11:是的 C++14:是的 C++17:否(有 explicit 构造函数) C++20:否(具有用户声明的构造函数)

C++17 增加了聚合不能有 explicit 构造函数的要求。


struct S 
    int x42;
    S() = delete;
;
C++11:否(具有默认成员初始化程序) C++14:是的 C++17:是的 C++20:否(具有用户声明的构造函数)

C++14 删除了 C++11 对聚合没有默认成员初始值设定项的要求。


struct S 
    int x42;
    explicit S() = delete;
;
C++11:否(具有默认成员初始化程序) C++14:是的 C++17:否(有 explicit 构造函数) C++20:否(具有用户声明的构造函数)

【讨论】:

以上是关于为啥在已删除的默认 ctor 旁边定义一个空的副本 ctor 会使空列表的值初始化失败?的主要内容,如果未能解决你的问题,请参考以下文章

在`dctor、copy ctor和copy assignment operator`中,为啥删除一个而让另一个隐式定义最有可能导致错误

为啥工会成员数据没有默认初始化?

为啥具有默认参数 std::initializer_list 的 ctor 不可用(VS2019)?

Git - 为啥在已删除文件上运行命令时需要双破折号?

为啥在初始化列表中没有创建额外副本的情况下分配数据?

new和delete,p150