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

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

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

为什么不直接说:

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

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

103146 次浏览

默认的默认构造函数被明确定义为与用户定义的默认构造函数相同,没有初始化列表和空的复合语句。

§12.1/6[类别]默认且未定义为删除的默认构造函数在ODR用于创建其类类型的对象时或在其第一次声明后显式默认时被隐式定义。隐式定义的默认构造函数执行类的一组初始化,这组初始化将由用户为该类编写的默认构造函数执行,没有ctor-initializer(12.6.2)和空复合语句.[..]

但是,虽然两个构造函数的行为相同,但提供空实现确实会影响类的某些属性。给定一个用户定义的构造函数,即使它什么也不做,也会使该类型不是骨料,也不是琐碎的。如果你想让你的类成为一个聚合类型或普通类型(或者通过传递性,成为POD类型),那么你需要使用= default

§8.5.1/1[DCL.INIT.AGGR]聚合是没有用户提供的构造函数[和..]的数组或类

§12.1/5[类别]如果默认构造函数不是用户提供的并且[..],则它是普通的。

§9/6[类别]平凡类是具有平凡默认构造函数和[..]的类。

要演示:

#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)语法,通过显式地说明您的意图,使您的代码更易于阅读。

N2210提供了一些原因:

违约的管理有几个问题:

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

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

在某些情况下,无需更改成员函数定义即可更改类体,因为默认值随 增加成员的声明。

请参阅在C++11中,三的规则变成了五的规则。

请注意,移动构造函数和移动赋值操作符不会 为显式声明任何其他 特殊成员函数,即复制构造函数和复制赋值函数 运算符不会为显式声明 移动构造函数或移动赋值运算符,并且具有 显式声明的析构函数和隐式定义的复制构造函数 或隐式定义的复制赋值运算符 已弃用

在某些情况下,这是一个语义问题。对于默认构造函数,这一点不是很明显,但对于其他编译器生成的成员函数,这一点就很明显了。

对于默认构造函数,可以将任何具有空主体的默认构造函数视为普通构造函数的候选,就像使用=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。它是ABC_1的_,用于克服被抑制的编译器生成的成员函数,如复制/移动构造函数和赋值操作符。然后,让它也为默认构造函数工作是“显而易见的”。

如果只是为了在某些情况下使较旧的代码更优化,使具有空主体的默认构造函数和普通成员/基本构造函数也被视为普通,就像它们在=default中一样,这可能是很好的,但是大多数依赖于普通默认构造函数进行优化的低级代码也依赖于普通复制构造函数。如果您必须去“修复”所有旧的复制构造函数,那么修复所有旧的默认构造函数也不是什么难事。使用明确的=default来表示您的意图也更清楚、更明显。

编译器生成的成员函数还会做一些其他的事情,您也必须显式地进行更改以支持这些事情。支持默认构造函数的constexpr就是一个例子。使用=default比使用=default所隐含的所有其他特殊关键字来标记函数更容易,这也是C++11的主题之一:使语言更容易。它仍然有很多缺点和缺点,但很明显,在易用性方面,它比C++03前进了一大步。

我有一个例子可以说明不同之处:

#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()会初始化成员。

由于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仍然是标准布局。

通过new T()创建对象时,存在显著差异。在默认构造函数的情况下,将进行聚合初始化,将所有成员值初始化为默认值。如果构造函数为空,则不会发生这种情况。(new T也不会发生这种情况)

考虑以下类:

struct T {
T() = default;
T(int x, int c) : s(c) {
for (int i = 0; i < s; i++) {
d[i] = x;
}
}
T(const T& o) {
s = o.s;
for (int i = 0; i < s; i++) {
d[i] = o.d[i];
}
}
void push(int x) { d[s++] = x; }
int pop() { return d[--s]; }


private:
int s = 0;
int d[1<<20];
};

new T()将所有成员初始化为零,包括4 MIB数组(memset到0,在GCC的情况下)。在这种情况下,这显然是不希望的,定义一个空的构造函数T() {}将会阻止这一点。

事实上,我曾经遇到过这种情况,当时Clion建议将T() {}替换为T() = default。它导致了显著的性能下降和调试/基准测试时间。

因此,我更喜欢使用空的构造函数,除非我真的希望能够使用聚合初始化。