使用花括号初始化列表的已删除构造函数的默认构造方式

4
假设我想禁用类的构建,那么我可以按照以下方式进行操作(参考最佳风格删除所有构造函数(或其他函数)?):
// This results in Example being CopyConstructible:
struct Example {
  Example() = delete;
};

或者

struct Example {
  template <typename... Ts>
  Example(Ts&&...) = delete;
};

或者

struct Example {
  Example(const Example&) = delete;
};

第一个示例如果被构建,则仍可复制(意图是禁用),但后两个将禁用几乎所有创建Example的方法。如果我使用空括号初始化列表默认构造上述任何一个实例,则该实例将成功构造。在Meyer的《Effective Modern C ++》中,他给出了以下示例(第51页底部):

Widget w3{}; // calls Widget ctor with no args

我预计上述的Example类将会失败,具体如下:

Example e{};

不应该构造,因为它应该调用已删除的默认构造函数。但是,它可以使用,并且如果定义为上述第一种情况,则也可以复制。请参见实时演示。我的问题是:这是否正确,如果正确,为什么?此外,如果这是正确的,如何完全禁用类的销毁?
1个回答

11

从零开始

首先,我们将澄清何为初始化对象以及构造函数何时/如何/是否被调用。
以下是我对标准的通俗解释,为了简洁起见,省略或修改了一些无关紧要的细节。

初始化器

初始化器有以下几种类型:

()     // parentheses
       // nothing
{}     // braced initializer list
= expr // assignment expression
括号和大括号初始化列表可能包含进一步的表达式。它们的使用方式如下所示,假设有结构体S.
new S()        // empty parentheses
S s(1, 2)      // parentheses with expression list as (1, 2)
S s            // nothing
S s{}          // empty braced initializer list
S s{{1}, {2}}  // braced initializer list with sublists
S s = 1        // assignment
S s = {1, 2}   // assignment with braced initializer list

请注意,我们尚未提到构造函数

初始化

根据使用的初始化程序执行初始化。

new S()        // value-initialize
S s(1, 2)      // direct-initialize
S s            // default-initialize
S s{}          // list-initialize
S s{{1}, {2}}  // list-initialize
S s = 1        // copy-initialize
S s = {1, 2}   // list-initialize

一旦初始化完成,该对象被视为已初始化。

请注意,构造函数再次未被提及

List initialize

我们将主要解释什么是列表初始化,因为这是问题所在。

当发生列表初始化时,以下按顺序考虑:

  1. 如果对象是聚合类型,且列表具有一个单独的元素,该元素是对象类型或源自对象类型,则使用该元素初始化对象。
  2. 如果对象是聚合类型,则进行聚合初始化。
  3. 如果列表为空,并且对象具有默认构造函数,则该对象进行值初始化(最终调用默认构造函数)。
  4. 如果对象是类类型,则考虑构造函数,使用列表的元素执行重载决策。

Aggregate

聚合类型定义如[dcl.init.aggr]所述:

聚合是具有以下特征的数组或类:
--没有用户提供的、显式的或继承的构造函数
--没有非静态数据成员的私有或保护成员
--没有虚函数和虚拟、私有或保护基类

具有删除的构造函数计入提供构造函数的部分。

聚合的元素定义如下:

聚合的元素有:
--对于数组,是按递增下标顺序的数组元素;或者
--对于类,是按声明顺序的直接基类,后跟未成为匿名联合成员的直接非静态数据成员,按声明顺序排列。

聚合初始化定义如下:

[...]初值列表的元素将按顺序作为聚合元素的初始值。

Example e{}示例

遵循上述规则,Example e{}之所以合法,是因为

the initializer is a braced initializer list
uses list initialization
since Example is an aggregate type
uses aggregate initialization
and therefore does not invoke any constructor
当你编写Example e{}时,它并没有进行默认构造,而是聚合初始化。所以,没问题。
事实上,以下内容可以编译通过。
struct S
{
    S() = delete;
    S(const S&) = delete;
    S(S&&) = delete;
    S& operator=(const S&) = delete;
};

S s{};  //perfectly legal

关闭构造函数

确保Example不是一个聚合类型,以停止聚合初始化并删除其构造函数。

这通常很简单,因为大多数类具有私有或受保护的数据成员。因此,在C++中经常忘记了聚合初始化存在的事实。

使一个类非聚合的最简单方法是:

struct S
{
    explicit S() = delete;
};
S s{};  //illegal, calls deleted default constructor

然而,截至2017年5月30日,只有gcc 6.1及以上版本和clang 4.0.0会拒绝此代码,所有版本的CL和icc都将错误地接受此代码。

其他初始化

这是C++中最疯狂的角落之一,查阅标准以理解发生了什么事情是相当费劲但也具有启发性的。已经有许多参考资料可供阅读,我不打算再尝试解释它们。


好的,那就是我想的,但是通过删除构造函数,所有隐式声明的构造函数都应该被删除。因此不应该有可以用于聚合初始化的构造函数,我仍然应该得到编译器错误。那么为什么我仍然可以构造这个类呢?问题已经编辑过了。 - RobClucas
@RobClucas 这是给你的。 - Passer By
谢谢!所以为了实现我想要的目标,在C++11之前通过将构造函数设为私有来禁用构造,会同时禁用聚合和非聚合类的构造和聚合初始化。 - RobClucas
哦,我误解了你的意思。是的,私有构造函数禁用了构造。不过,这比删除构造函数要难理解一些。 - Passer By
是的,我更喜欢使用“delete”,但由于我的类是聚合的,所以我没有太多选择! - RobClucas
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接