C ++ 11中的新语法“ = default”


136

我不明白为什么要这么做:

struct S { 
    int a; 
    S(int aa) : a(aa) {} 
    S() = default; 
};

为什么不说:

S() {} // instead of S() = default;

为什么要为此引入新的语法?


30
Nitpick:default不是新的关键字,它只是已经保留的关键字的新用法。


梅伊是这个问题可以帮助你。
FreeNickname 2013年

7
除了其他答案外,我还认为“ = default;” 更自我记录。
2013年

Answers:


136

默认的默认构造函数专门定义为与没有初始化列表且空复合语句的用户定义的默认构造函数相同。

§12.1/ 6 [class.ctor]当默认创建器被默认用于创建其类类型的对象时,或者在其首次声明后被明确默认为默认值时,将默认定义为默认且未定义为Delete的默认构造函数。隐式定义的默认构造函数执行该类的初始化集,该初始化集将由用户为该类编写的默认构造函数执行,而没有ctor-initializer(12.6.2)和一个空的复合语句。[...]

但是,尽管两个构造函数的行为相同,但是提供空的实现确实会影响该类的某些属性。提供一个用户定义的构造函数,即使它什么也不做,会使该类型不是聚合的,也不是琐碎的。如果您希望类是聚合类型或琐碎类型(或通过传递性,则是POD类型),则需要使用= default

§8.5.1/ 1 [dcl.init.aggr]聚合是没有用户提供的构造函数的数组或类,[和...]

§12.1/ 5 [class.ctor]如果默认构造函数不是用户提供的,则它是微不足道的,并且[...]

§9/ 6 [class]普通类是具有普通默认构造函数和[...]的类

展示:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() { };
};

int main() {
    static_assert(std::is_trivial<X>::value, "X should be trivial");
    static_assert(std::is_pod<X>::value, "X should be POD");
    
    static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
    static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

此外,constexpr如果隐式构造函数本来可以显式默认构造函数,那么它将默认设置为默认值,并且还将为其赋予与隐式构造函数相同的异常规范。在您给出的情况下,隐式构造函数将不会constexpr(因为它将使数据成员保持未初始化状态),并且也将具有空的异常规范,因此没有区别。但是可以,在一般情况下,您可以手动指定constexpr和异常说明以匹配隐式构造函数。

使用= default确实带来了一些统一性,因为它也可以与复制/移动构造函数和析构函数一起使用。例如,空副本构造函数将与默认的副本构造函数(它将执行其成员的成员级复制)不同。对这些特殊成员函数中的每一个统一使用= default(或= delete)语法,通过明确说明您的意图使代码更易于阅读。


几乎。12.1 / 6:“如果该用户编写的默认构造函数可以满足构造函数的要求constexpr(7.1.5),则隐式定义的默认构造函数为constexpr。”
Casey 2013年

实际上,8.4.2 / 2更具信息性:“如果一个函数在其第一个声明中被显式默认,(a)隐式认为constexpr该隐式声明将是,(b)隐式认为其具有相同的声明。异常说明,就好像它已被隐式声明一样(15.4),...“在这种特定情况下,它没有什么区别,但是通常foo() = default;比稍有优势foo() {}
Casey 2013年

2
您说没有差异,然后继续解释差异?

@hvd在这种情况下没有区别,因为隐式声明不会constexpr(因为未初始化数据成员),并且其异常规范允许所有异常。我会说得更清楚。
约瑟夫·曼斯菲尔德

2
感谢您的澄清。不过,两者之间似乎仍然有所不同constexpr(您提到的在这里不应有所不同):struct S1 { int m; S1() {} S1(int m) : m(m) {} }; struct S2 { int m; S2() = default; S2(int m) : m(m) {} }; constexpr S1 s1 {}; constexpr S2 s2 {};s1给出错误,而不是s2。在clang和g ++中。

10

我有一个例子,将显示差异:

#include <iostream>

using namespace std;
class A 
{
public:
    int x;
    A(){}
};

class B 
{
public:
    int x;
    B()=default;
};


int main() 
{ 
    int x = 5;
    new(&x)A(); // Call for empty constructor, which does nothing
    cout << x << endl;
    new(&x)B; // Call for default constructor
    cout << x << endl;
    new(&x)B(); // Call for default constructor + Value initialization
    cout << x << endl;
    return 0; 
} 

输出:

5
5
0

如我们所见,对空A()构造函数的调用不会初始化成员,而B()会初始化成员。


7
您能否解释一下这种语法-> new(&x)A();
Vencat

5
我们正在从变量x的地址开始的内存中创建新对象(而不是新的内存分配)。该语法用于在预分配的内存中创建对象。由于在我们的情况下B的大小= int的大小,因此new(&x)A()将在x变量的位置创建新对象。
Slavenskij

感谢您的解释。
Vencat

1
我在gcc 8.3中得到了不同的结果:ideone.com/XouXux
Adam.Er8

即使使用C ++ 14,我也会
Mayank Bhushan

9

n2210提供了一些原因:

默认管理有几个问题:

  • 构造函数定义是耦合的;声明任何构造函数都会抑制默认构造函数。
  • 析构函数的默认值不适用于多态类,因此需要显式定义。
  • 一旦取消默认设置,就没有办法恢复默认设置。
  • 默认实现通常比手动指定的实现更有效。
  • 非默认实现是不平凡的,这会影响类型语义,例如使类型成为非POD。
  • 如果没有声明(非平凡的)替代,就无法禁止特殊的成员函数或全局运算符。

type::type() = default;
type::type() { x = 3; }

在某些情况下,类主体可以更改而无需更改成员函数定义,因为默认值随其他成员的声明而更改。

看到三规则成为C ++ 11的五规则吗?

请注意,不会为显式声明任何其他特殊成员函数的类生成move构造函数和move赋值运算符,对于显式声明了move构造函数或move的类不会生成copy构造函数和copy赋值运算符赋值运算符,并且具有显式声明的析构函数和隐式定义的副本构造函数或隐式定义的副本赋值运算符的类被视为已弃用


1
它们是= default通常具有原因的原因,而不是= default对构造函数进行比较的原因{ }
约瑟夫·曼斯菲尔德

@JosephMansfield没错,但既然{}已经推出之前的语言的一个特征=default,这些原因毫无保留地依赖的区别(例如:“有复活[抑制的默认]绝非”意味着{}等同于默认)。
凯尔·斯特兰德

7

在某些情况下,这是语义问题。在默认构造函数中,这不是很明显,但是在其他编译器生成的成员函数中,它变得很明显。

对于默认构造函数,可以将任何具有空主体的默认构造函数视为使用琐碎构造函数的候选对象,与using相同=default。毕竟,旧的空默认构造函数是合法的C ++

struct S { 
  int a; 
  S() {} // legal C++ 
};

在大多数优化之外的情况下(手动或编译器优化),编译器是否理解此构造函数无关紧要。

但是,这种将空函数体视为“默认”的尝试完全破坏了其他类型的成员函数。考虑复制构造函数:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

在上述情况下,用空主体编写的副本构造函数现在是错误的。它实际上不再是复制任何东西。与默认的复制构造函数语义相比,这是一组非常不同的语义。所需的行为要求您编写一些代码:

struct S { 
  int a; 
  S() {}
  S(const S& src) : a(src.a) {} // fixed
};

但是,即使是这种简单情况,编译器也要验证副本构造函数与它自己生成的副本构造函数相同,或者让它看到副本构造函数是琐碎的(相当于memcpy基本上, )。编译器将必须检查每个成员初始化器表达式,并确保该表达式与该表达式相同,以访问源的相应成员,并且别无其他选择,确保没有任何成员具有非平凡的默认构造,等等。在过程中它是向后的编译器将用来验证该函数自己生成的版本是微不足道的。

然后考虑复制赋值运算符,该赋值运算符可以变得更加毛茸茸,尤其是在非平凡情况下。您不需要为许多类编写大量的样板,但是无论如何,您都必须在C ++ 03中被迫这样做:

struct T { 
  std::shared_ptr<int> b; 
  T(); // the usual definitions
  T(const T&);
  T& operator=(const T& src) {
    if (this != &src) // not actually needed for this simple example
      b = src.b; // non-trivial operation
    return *this;
};

那是一个简单的例子,但是它已经比强迫您为这样的简单类型编写的代码要多了T(尤其是一旦我们将移动操作扔进混合中时)。我们不能依靠空的主体来表示“填写默认值”,因为空的主体已经完全有效并且具有明确的含义。实际上,如果用空主体表示“填写默认值”,那么就没有办法显式地创建无操作拷贝构造函数等。

这又是一致性的问题。空的主体意味着“不执行任何操作”,但是对于诸如复制构造函数之类的操作,您实际上不希望“不执行任何操作”,而是“执行所有通常会被抑制的操作”。因此=default。克服抑制的编译器生成的成员函数(如复制/移动构造函数和赋值运算符)是必要的。然后,它也“显而易见”以使其也适用于默认构造函数。

最好将具有空主体的默认构造函数和琐碎的成员/基部构造函数也视为琐碎的,就像=default在某些情况下只是为了使较旧的代码更优化时一样,将它们视为琐碎的事情,但是大多数低级代码都依赖琐碎的用于优化的默认构造函数也依赖于琐碎的副本构造函数。如果您必须去“修复”所有旧的副本构造函数,那么修复所有旧的默认构造函数实际上也不是一件容易的事。使用显式=default表示您的意图也更加清晰和明显。

编译器生成的成员函数还会执行其他一些操作,您还必须显式进行更改以支持该功能。支持constexpr默认构造函数就是一个示例。从心理上讲,使用它=default比用所有其他特殊关键字标记函数要容易得多,=default这是C ++ 11的主题之一:使语言更容易。它仍然有很多缺陷和反向兼容的折衷,但是很明显,就易用性而言,这是从C ++ 03向前迈出的一大步。


我遇到了一个我预期= default会实现的问题,a=0;而并非如此!我不得不放弃它而赞成: a(0)。我仍然对您的用处感到困惑= default,这与性能有关吗?如果我不使用它会在某处破裂= default吗?我尝试在这里阅读所有答案购买我对某些c ++东西是陌生的,并且在理解它方面遇到很多麻烦。
水瓶座力量

@AquariusPower:这不仅与性能有关,而且在某些情况下还涉及异常和其他语义。也就是说,默认操作符可能很琐碎,但非默认操作符永远不会很简单,并且某些代码将使用元编程技术来更改非琐碎操作的行为甚至禁止类型。您的a=0示例是由于琐碎类型的行为,它们是一个单独的主题(尽管相关)。
肖恩·米德迪奇

确实这意味着它可能有= default,仍然资助a=0?某种程度上来说?您是否认为我可以创建一个新问题,例如“如何具有构造函数= default并授予字段将被正确初始化?”,顺便说一句,我在a struct而不是a中遇到了问题class,并且即使不使用该应用程序也可以正常运行= default,我可以在这个问题上添加一个最小的结构,如果它是一个好问题:)
Aquarius Power

1
@AquariusPower:您可以使用非静态数据成员初始化程序。像这样编写您的结构:struct { int a = 0; };如果您随后决定需要一个构造函数,则可以将其默认设置,但是请注意,类型不会很琐碎(这很好)。
肖恩·米德迪奇

2

由于的弃用std::is_pod及其替代方案std::is_trivial && std::is_standard_layout,@ JosephMansfield的答案中的摘录变为:

#include <type_traits>

struct X {
    X() = default;
};

struct Y {
    Y() {}
};

int main() {
    static_assert(std::is_trivial_v<X>, "X should be trivial");
    static_assert(std::is_standard_layout_v<X>, "X should be standard layout");

    static_assert(!std::is_trivial_v<Y>, "Y should not be trivial");
    static_assert(std::is_standard_layout_v<Y>, "Y should be standard layout");
}

请注意,Y仍然是标准布局。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.