删除了默认构造函数.仍然可以创建对象......有时候

bol*_*lov 49 c++ language-lawyer aggregate-initialization list-initialization c++14

c ++ 11统一初始化语法的天真,乐观和哦......错误的观点

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

好处是:没有狭义的隐式转换,最烦人的解析没有问题,一致性(?).我没有看到任何问题,因为我认为它们是相同的(除了给出的例子).

但他们不是.

一个纯粹疯狂的故事

{}调用默认的构造函数.

......除非:

  • 删除默认构造函数
  • 没有定义其他构造函数.

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

......除非:

  • 该对象有一个删除的默认构造函数和
  • 其他构造函数定义.

然后失败了call to deleted constructor.

......除非:

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

然后失败并丢失了字段初始值设定项.

但是,您可以使用它{value}来构造对象.

好吧也许这与第一个异常相同(值init对象)

......除非:

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

然后也{}不能{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.
Run Code Online (Sandbox Code Playgroud)

情景B

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

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

foo f{}; // OK
Run Code Online (Sandbox Code Playgroud)

情景C.

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

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

foo f{}; // error call to deleted constructor
Run Code Online (Sandbox Code Playgroud)

情景D.

删除了默认构造函数,没有定义其他构造函数,数据成员

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

foo f{}; // error use of deleted function foo::foo()
foo f{3}; // OK
Run Code Online (Sandbox Code Playgroud)

情景E.

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

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

foo f{}; // ERROR: missing initializer
foo f{3}; // OK
Run Code Online (Sandbox Code Playgroud)

情景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)`
Run Code Online (Sandbox Code Playgroud)

bol*_*lov 35

当以这种方式查看事物时,很容易说对象初始化的方式存在完全和彻底的混乱.

最大的区别来自于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; DR

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

  • 对于聚合,每个数据成员都从列表初始值设定项的元素初始化
  • else调用构造函数

这不是打败了删除的构造函数的全部目的吗?

好吧,我不知道这一点,但解决方案是foo不要聚合.最常见的形式不增加任何开销并且不改变对象的使用语法是使它从空结构继承:

struct dummy_t {};

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

foo f{}; // ERROR call to deleted constructor
Run Code Online (Sandbox Code Playgroud)

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

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

foo f{}; // ERROR use of deleted function `foo::~foo()`
Run Code Online (Sandbox Code Playgroud)

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

非常感谢@MM帮助纠正和改进这篇文章.

  • C++ 17将允许聚合具有公共基础,因此您需要将`dummy_t`设为私有(或受保护,但我不明白这一点). (2认同)

M.M*_*M.M 7

弄乱你的是聚合初始化.

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

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


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

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

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

这个缺陷在修改C++ 14 的缺陷报告中修复,因此从现在开始,在我们进入聚合初始化之前检查复制构造.


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

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

在C++ 11中,非静态成员的默认值意味着一个类不是聚合; 但是C++ 14改变了. 用户提供的意思是用户声明的,但不是= default= delete.


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


Sha*_*our 6

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

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

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

struct X {
    X() = delete;
  };

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

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

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

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

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

 static_assert(!std::is_default_constructible_v<X>);
Run Code Online (Sandbox Code Playgroud)

将通过上述 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段如下:

[注意:聚合数组或聚合类可能包含类 >> 类型的元素 用户提供 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 中修复。