我不明白为什么要这么做:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
为什么不说:
S() {} // instead of S() = default;
为什么要为此引入新的语法?
我不明白为什么要这么做:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
为什么不说:
S() {} // instead of S() = default;
为什么要为此引入新的语法?
Answers:
默认的默认构造函数专门定义为与没有初始化列表且空复合语句的用户定义的默认构造函数相同。
§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
)语法,通过明确说明您的意图使代码更易于阅读。
constexpr
(7.1.5),则隐式定义的默认构造函数为constexpr
。”
constexpr
该隐式声明将是,(b)隐式认为其具有相同的声明。异常说明,就好像它已被隐式声明一样(15.4),...“在这种特定情况下,它没有什么区别,但是通常foo() = default;
比稍有优势foo() {}
。
constexpr
(因为未初始化数据成员),并且其异常规范允许所有异常。我会说得更清楚。
我有一个例子,将显示差异:
#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::type() = default; type::type() { x = 3; }
在某些情况下,类主体可以更改而无需更改成员函数定义,因为默认值随其他成员的声明而更改。
请注意,不会为显式声明任何其他特殊成员函数的类生成move构造函数和move赋值运算符,对于显式声明了move构造函数或move的类不会生成copy构造函数和copy赋值运算符赋值运算符,并且具有显式声明的析构函数和隐式定义的副本构造函数或隐式定义的副本赋值运算符的类被视为已弃用
= default
通常具有原因的原因,而不是= default
对构造函数进行比较的原因{ }
。
{}
已经推出之前的语言的一个特征=default
,这些原因都毫无保留地依赖的区别(例如:“有复活[抑制的默认]绝非”意味着{}
是不等同于默认)。
在某些情况下,这是语义问题。在默认构造函数中,这不是很明显,但是在其他编译器生成的成员函数中,它变得很明显。
对于默认构造函数,可以将任何具有空主体的默认构造函数视为使用琐碎构造函数的候选对象,与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 ++东西是陌生的,并且在理解它方面遇到很多麻烦。
a=0
示例是由于琐碎类型的行为,它们是一个单独的主题(尽管相关)。
= default
,仍然资助a
会=0
?某种程度上来说?您是否认为我可以创建一个新问题,例如“如何具有构造函数= default
并授予字段将被正确初始化?”,顺便说一句,我在a struct
而不是a中遇到了问题class
,并且即使不使用该应用程序也可以正常运行= default
,我可以在这个问题上添加一个最小的结构,如果它是一个好问题:)
struct { int a = 0; };
如果您随后决定需要一个构造函数,则可以将其默认设置,但是请注意,类型不会很琐碎(这很好)。
由于的弃用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
仍然是标准布局。
default
不是新的关键字,它只是已经保留的关键字的新用法。