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

时间:2015-11-29 21:22:01

标签: c++ c++14 language-lawyer list-initialization aggregate-initialization

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.

情景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)`

3 个答案:

答案 0 :(得分:33)

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

最大的区别来自foo的类型:如果它是聚合类型。

  

如果它有:

,它是一个聚合      
      
  • 没有用户提供的构造函数(已删除或默认的函数不计入用户提供的内容),
  •   
  • 没有私有或受保护的非静态数据成员,
  •   
  • 非静态数据成员没有大括号或等号初始化者(因为c ++ 11直到(还原于)c ++ 14)
  •   
  • 没有基类,
  •   
  • 没有虚拟成员功能。
  •   

所以:

    场景中的
  • A B D E:foo是聚合
  • 方案C中的
  • foo不是聚合
  • 情景F:
    • 在c ++ 11中它不是聚合。
    • 在c ++ 14中它是一个聚合。
    • g ++尚未实现此功能,即使在C ++ 14中仍然将其视为非聚合。
      • 4.9没有实现此目的。
      • 5.2.0
      • 5.2.1 ubuntu没有(可能是回归)
  

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

     
      
  • ...
  •   
  • 如果T是聚合类型,则执行聚合初始化。这将处理场景A B D E(和C ++ 14中的F)
  •   
  • 否则T的构造函数分为两个阶段:      
        
    • 所有采用std :: initializer_list ...
    • 的构造函数   
    • 否则[...] T的所有构造函数都参与重载决策[...]这将照顾C(和C ++ 11中的F)
    •   
  •   
  • ...
  •   

  

聚合T类型对象的初始化(方案A B D E(F c ++ 14)):

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

<强> TL; DR

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

  • 对于聚合,每个数据成员都是从列表初始化程序的元素
  • 初始化的
  • else call constructor
  

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

好吧,我不知道这一点,但解决办法是让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()`

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

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

答案 1 :(得分:7)

搞乱你的是聚合初始化

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

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

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

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

上面确实存在一个设计缺陷:如果我们有T t1; T t2{t1};,那么意图是执行复制构造。但是,(在C ++ 14之前),如果T是聚合,则会发生聚合初始化,而t2的第一个成员将使用t1进行初始化。

这个缺陷修复了defect report,它修改了C ++ 14,所以从现在开始,在我们进行聚合初始化之前检查复制构造。

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

  

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

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

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

答案 2 :(得分:4)

这些与聚合初始化有关的情况对于大多数人来说是违反直觉的,并且是建议p1008: Prohibit aggregates with user-declared constructors的主题,该建议说:

  

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段修改如下:

     
    

[注意:聚合数组或聚合类可能包含类>> >>具有 用户提供的 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代码,可汇总初始化     用户声明的构造函数的类型可能格式错误或具有     该国际标准中的不同语义。

  

下面是我省略的示例。

建议是accepted and merged into C++20,我们可以找到包含这些更改的latest draft here,并且可以看到[dcl.init.aggr]p1.1[dcl.init.aggr]p17C++17 declarations diff的更改。 / p>

因此,这应该在C ++ 20及更高版本中解决。