我不明白为什么我会这样做:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
为什么不直接说:
S() {} // instead of S() = default;
为什么要为此引入新语法?
default
不是新关键字,它只是对已保留关键字的新用法。
默认的默认构造函数被明确定义为与没有初始化列表和空复合语句的用户定义的默认构造函数相同。
§12.1/6 [class.ctor] 默认构造函数被默认且未定义为已删除,当它被用于创建其类类型的对象或在其第一次声明后显式默认时,它被隐式定义。隐式定义的默认构造函数执行类的一组初始化,这些初始化将由用户编写的该类的默认构造函数执行,没有 ctor-initializer (12.6.2) 和空的复合语句。 [...]
但是,虽然两个构造函数的行为相同,但提供空实现确实会影响类的某些属性。给出一个用户定义的构造函数,即使它什么都不做,也使得类型不是一个聚合,也不是trivial。如果您希望您的类是聚合类型或平凡类型(或通过传递性,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
)语法,通过明确说明您的意图,使您的代码更易于阅读。
我有一个例子可以显示差异:
#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() 会这样做。
n2210 提供了一些原因:
默认值的管理有几个问题: 构造函数定义是耦合的;声明任何构造函数都会抑制默认构造函数。析构函数默认值不适用于多态类,需要显式定义。一旦默认值被抑制,就无法恢复它。默认实现通常比手动指定的实现更有效。非默认实现是不平凡的,它会影响类型语义,例如使类型成为非 POD。如果不声明(非平凡的)替代品,就无法禁止特殊成员函数或全局运算符。
类型::type() = 默认;类型::类型() { x = 3;在某些情况下,类主体可以更改而无需更改成员函数定义,因为默认值会随着附加成员的声明而更改。
请参阅Rule-of-Three becomes Rule-of-Five with C++11?:
请注意,不会为显式声明任何其他特殊成员函数的类生成移动构造函数和移动赋值运算符,不会为显式声明移动构造函数或移动的类生成复制构造函数和复制赋值运算符赋值运算符,并且具有显式声明的析构函数和隐式定义的复制构造函数或隐式定义的复制赋值运算符的类被视为已弃用
= default
的原因,而不是在构造函数上执行 = default
与执行 { }
的原因。
{}
在引入 =default
之前已经是语言的一个特征,这些原因确实隐含地依赖于区别(例如“没有办法复活 [抑制的默认值]" 意味着 {}
不 等同于默认值)。
在某些情况下,这是语义问题。对于默认构造函数,这不是很明显,但对于其他编译器生成的成员函数,它变得很明显。
对于默认构造函数,可以将任何具有空主体的默认构造函数视为普通构造函数的候选者,与使用 =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++ 的东西很陌生,我在理解它时遇到了很多麻烦。
a=0
示例是因为琐碎类型的行为,这是一个单独的(尽管相关的)主题。
= default
并且仍然授予 a
将是 =0
?某种程度上来说?您认为我可以创建一个新问题,例如“如何拥有构造函数 = default
并授予字段将被正确初始化?”,顺便说一句,我在 struct
而不是 class
中遇到了问题,并且应用程序即使不使用 = default
也可以正常运行,如果它是一个好的问题,我可以在该问题上添加一个最小结构:)
struct { int a = 0; };
如果你决定需要一个构造函数,你可以默认它,但请注意类型不会是 trivial (这很好)。
由于弃用 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 数组(在 gcc 的情况下 memset
为 0)。在这种情况下这显然是不希望的,定义一个空的构造函数 T() {}
会阻止这种情况。
事实上,当 CLion 建议将 T() {}
替换为 T() = default
时,我曾遇到过这种情况。它导致了显着的性能下降和数小时的调试/基准测试。
所以我毕竟更喜欢使用空的构造函数,除非我真的希望能够使用聚合初始化。
不定期副业成功案例分享
constexpr
构造函数 (7.1.5) 的要求,则隐式定义的默认构造函数是constexpr
。”constexpr
,(b) 它被隐式认为是具有相同的异常规范,就好像它已被隐式声明 (15.4),..." 在这种特定情况下没有区别,但总的来说foo() = default;
比foo() {}
有一点优势。constexpr
(因为数据成员未初始化)并且其异常规范允许所有异常。我会更清楚地说明这一点。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++ 中。