56

对 c++11 统一初始化语法的天真、乐观和哦.. 如此错误的看法

我认为既然 C++11 用户定义类型对象应该使用新{...}语法而不是旧(...)语法构造(除了构造函数重载 forstd::initializer_list和类似参数(例如std::vector:size ctor vs 1 elem init_list ctor))。

好处是:没有狭窄的隐式转换,最麻烦的解析没有问题,一致性(?)。我认为没有问题,因为我认为它们是相同的(除了给出的示例)。

但他们不是。

一个纯粹疯狂的故事

{}调用默认构造函数。

... 除非:

  • 默认构造函数被删除并且
  • 没有定义其他构造函数。

然后看起来它宁愿值初始化对象?...即使对象已经删除了默认构造函数,{}也可以创建一个对象。这难道不是破坏了已删除构造函数的全部目的吗?

...除了以下情况:

  • 该对象有一个已删除的默认构造函数,并且
  • 定义了其他构造函数。

然后它失败了call to deleted constructor

...除了以下情况:

  • 该对象有一个已删除的构造函数,并且
  • 没有定义其他构造函数并且
  • 至少是一个非静态数据成员。

然后它因缺少字段初始化程序而失败。

但是你可以使用它{value}来构造对象。

好的,也许这与第一个异常相同(值初始化对象)

...除了以下情况:

  • 该类有一个已删除的构造函数
  • 并且至少有一个数据成员在类内默认初始化。

那么也不{}{value}不能创建对象。

我确定我错过了一些。具有讽刺意味的是,它被称为统一初始化语法。我再说一遍:UNIFORM初始化语法。

这是什么疯狂?

方案 A

删除了默认构造函数:

struct foo {
  foo() = delete;
};

// All bellow OK (no errors, no warnings)
foo f = foo{};
foo f = {};
foo f{}; // will use only this from now on.

方案 B

删除默认构造函数,删除其他构造函数

struct foo {
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // OK

方案 C

删除了默认构造函数,定义了其他构造函数

struct foo {
  foo() = delete;
  foo(int) {};
};

foo f{}; // error call to deleted constructor

方案 D

已删除默认构造函数,未定义其他构造函数,数据成员

struct foo {
  int a;
  foo() = delete;
};

foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK

方案 E

删除了默认构造函数,删除了 T 构造函数,T 数据成员

struct foo {
  int a;
  foo() = delete;
  foo(int) = delete;
};

foo f{}; // ERROR: missing initializer
foo f{3}; // OK

方案 F

删除了默认构造函数,类内数据成员初始化器

struct foo {
  int a = 3;
  foo() = delete;
};

/* Fa */ foo f{}; // ERROR: use of deleted function `foo::foo()`
/* Fb */ foo f{3}; // ERROR: no matching function to call `foo::foo(init list)`

4

3 回答 3

37

当以这种方式看待事物时,很容易说对象的初始化方式完全混乱。

最大的区别在于foo: 的类型是否是聚合类型。

它是一个聚合,如果它具有:

  • 没有用户提供的构造函数(已删除或默认的函数不算是用户提供的),
  • 没有私有或受保护的非静态数据成员,
  • 非静态数据成员没有大括号或等号初始化器(从 c++11 开始直到(恢复到)c++14)
  • 没有基类,
  • 没有虚拟成员函数。

所以:

  • 在场景中 ABDE:foo是一个聚合
  • 在场景 C 中:foo不是聚合
  • 场景 F:
    • 在 c++11 中,它不是聚合。
    • 在 c++14 中,它是一个聚合。
    • g++ 没有实现这一点,即使在 C++14 中仍然将其视为非聚合。
      • 4.9没有实现这一点。
      • 5.2.0
      • 5.2.1 ubuntu没有(也许是回归)

T 类型对象的列表初始化的效果是:

  • ...
  • 如果 T 是聚合类型,则执行聚合初始化。这需要处理场景 ABDE(和 C++14 中的 F)
  • 否则 T 的构造函数分为两个阶段:
    • 所有采用 std::initializer_list 的构造函数...
    • 否则 [...] T 的所有构造函数都参与重载决议 [...] 这会处理 C(以及 C++11 中的 F)
  • ...

T 类型对象的聚合初始化(场景 ABDE (F c++14)):

  • 每个非静态类成员,按照在类定义中的出现顺序,都是从初始化列表的相应子句复制初始化的。(数组引用省略)

TL;博士

所有这些规则看起来仍然非常复杂且令人头疼。我个人为自己过度简化了这一点(如果我因此在脚上开枪,那就这样吧:我想我会在医院呆 2 天,而不是有几十天的头痛):

  • 对于聚合,每个数据成员都是从列表初始化程序的元素初始化的
  • 否则调用构造函数

这难道不是破坏了已删除构造函数的全部目的吗?

好吧,我不知道,但解决方案是foo不要聚合。不增加开销且不更改对象使用的语法的最通用形式是使其继承自空结构:

struct dummy_t {};

struct foo : dummy_t {
  foo() = delete;
};

foo f{}; // ERROR call to deleted constructor

在某些情况下(我猜根本没有非静态成员),另一种方法是删除析构函数(这将使对象在任何上下文中都不可实例化):

struct foo {
  ~foo() = delete;
};

foo f{}; // ERROR use of deleted function `foo::~foo()`

此答案使用从以下位置收集的信息:

非常感谢@MM,他帮助纠正和改进了这篇文章。

于 2015-11-29T21:22:00.753 回答
7

让您感到困惑的是聚合初始化

正如您所说,使用列表初始化有利有弊。(C++ 标准不使用术语“统一初始化”)。

缺点之一是聚合与非聚合的列表初始化行为不同。此外,汇总的定义随每个标准略有变化。


聚合不是通过构造函数创建的。(从技术上讲,它们实际上可能是,但这是一种很好的思考方式)。相反,在创建聚合时,会分配内存,然后根据列表初始化程序中的内容按顺序初始化每个成员。

非聚合是通过构造函数创建的,在这种情况下,列表初始值设定项的成员是构造函数参数。

上面实际上有一个设计缺陷:如果我们有T t1; T t2{t1};,那么意图是执行复制构造。但是,(在 C++14 之前) ifT是一个聚合,然后聚合初始化发生,并且t2的第一个成员用t1.

此缺陷已在修改 C++14 的缺陷报告中得到修复,因此从现在开始,在进行聚合初始化之前检查复制构造。


C++14中聚合的定义是:

聚合是一个数组或一个类(第 9 条),没有用户提供的构造函数(12.1),没有私有或受保护的非静态数据成员(第 11 条),没有基类(第 10 条),也没有虚函数(10.3) )。

在 C++11 中,非静态成员的默认值意味着类不是聚合;但是对于 C++14,情况发生了变化。 User-provided表示用户声明,但不是= defaultor = delete


如果您想确保您的构造函数调用不会意外执行聚合初始化,那么您必须使用( )而不是{ },并以其他方式避免 MVP。

于 2015-11-29T22:41:48.863 回答
6

这些关于聚合初始化的案例对大多数人来说是违反直觉的,并且是提案p1008:禁止聚合与用户声明的构造函数的主题,其中说:

C++ 目前允许通过聚合初始化来初始化一些具有用户声明的构造函数的类型,绕过那些构造函数。结果是令人惊讶、混乱和错误的代码。本文提出了一种修复方法,使 C++ 中的初始化语义更安全、更统一且更易于教授。我们还讨论了此修复引入的重大更改

并介绍了一些示例,这些示例与您提出的案例很好地重叠:

struct X {
    X() = delete;
  };

 int main() {
    X x1;   // ill-formed - default c’tor is deleted
    X x2{}; // compiles!
}

显然,删除构造函数的目的是防止用户初始化类。然而,与直觉相反,这不起作用:用户仍然可以通过聚合初始化来初始化 X,因为这完全绕过了构造函数。作者甚至可以显式删除所有默认、复制和移动构造函数,但仍然无法阻止客户端代码通过上述聚合初始化来实例化 X。大多数 C++ 开发人员在显示此代码时对当前行为感到惊讶。 X 类的作者也可以考虑将默认构造函数设为私有。但是如果给这个构造函数一个默认定义,这也不会阻止类的聚合初始化(因此,实例化):

struct X {
  private:
    X() = default;
  };

int main() {
    X x1;     // ill-formed - default c’tor is private
    X x2{};  // compiles!
  }

由于当前的规则,聚合初始化允许我们“默认构造”一个​​类,即使它实际上不是可默认构造的:

 static_assert(!std::is_default_constructible_v<X>);

将通过上述 X 的两个定义。

...

建议的更改是:

修改[dcl.init.aggr]第1段如下:

聚合是一个数组或一个类(第 12 条)

  • 没有用户提供的显式u̲s̲e̲r̲ -̲d̲e̲c̲l̲a̲r̲e̲d̲或继承的构造函数 (15.1),

  • 没有私有或受保护的非静态数据成员(第 14 条),

  • 没有虚拟功能(13.3),和

  • 没有虚拟、私有或受保护的基类 (13.1)。

修改[dcl.init.aggr]第17段如下:

[注意:聚合数组或聚合类可能包含类 >>type 的元素用户提供 u̲s̲e̲r̲-̲d̲e̲c̲l̲a̲r̲e̲d̲构造函数(15.1)。>> 这些聚合对象的初始化在 15.6.1 中描述。——尾注]

将以下内容添加到附件 C 的 C.5 C++ 和 ISO C++ 2017 部分的 [diff.cpp17] 中:

C.5.6 第 11 条:声明符 [diff.cpp17.dcl.decl]

受影响的子条款:[dcl.init.aggr]
更改:具有用户声明的构造函数的类永远不是聚合。
基本原理:删除潜在的容易出错的聚合初始化,这些初始化可能适用于不承受类的声明构造函数的情况。
对原始功能的影响:在本国际标准中,使用用户声明的构造函数聚合初始化类型的有效 C++ 2017 代码可​​能格式错误或具有不同的语义。

后面是我省略的例子。

该提案被接受并合并到 C++20我们可以在这里找到包含这些更改的最新草案,我们可以看到对[dcl.init.aggr]p1.1[dcl.init.aggr]p17C的更改++17 声明差异

所以这应该在 C++20 中修复。

于 2018-07-27T21:31:01.580 回答