我不明白为什么我会这样做:
struct S {
int a;
S(int aa) : a(aa) {}
S() = default;
};
为什么不说:
S() {} // instead of S() = default;
为什么要为此引入一个新关键字?
答案 0 :(得分:117)
默认的默认构造函数具体定义为与用户定义的默认构造函数相同,没有初始化列表和空复合语句。
§12.1/ 6 [class.ctor] 默认构造函数是默认构造函数,默认构造函数是默认构造函数,当它用于创建类类型的对象或其时,它是隐式定义的在第一次声明后明确默认。隐式定义的默认构造函数执行该类的初始化集,该初始化集将由该用户编写的默认构造函数执行,该类没有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
)语法,通过明确说明您的意图,使代码更易于阅读。
答案 1 :(得分:9)
n2210提供了一些理由:
默认管理有几个问题:
- 构造函数定义是耦合的;声明任何构造函数都会抑制默认构造函数。
- 析构函数默认值不适合多态类,需要明确定义。
- 一旦默认被禁止,就无法复活它。
- 默认实现通常比手动指定的实现更有效。
- 非默认实现非常重要,它会影响类型语义,例如:使类型为非POD。
- 如果没有声明(非平凡的)替代品,则无法禁止特殊成员函数或全局运算符。
type::type() = default; type::type() { x = 3; }
在某些情况下,类主体可以更改而无需更改成员函数定义,因为默认值随之更改 宣布增加成员。
请参阅Rule-of-Three becomes Rule-of-Five with C++11?:
请注意,移动构造函数和移动赋值运算符不会 为明确声明其他任何一个的类生成 特殊成员函数,复制构造函数和复制赋值 不会为显式声明a的类生成运算符 移动构造函数或移动赋值运算符,并且该类具有 显式声明了析构函数和隐式定义的复制构造函数 或者考虑隐式定义的复制赋值运算符 弃用
答案 2 :(得分:5)
在某些情况下,这是一个语义问题。使用默认构造函数并不是很明显,但是使用其他编译器生成的成员函数就很明显了。
对于默认构造函数,可以使任何具有空体的默认构造函数被认为是一个简单的构造函数的候选者,与使用=default
相同。毕竟,旧的空默认构造函数是 legal 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的一大进步。
答案 3 :(得分:2)
由于弃用import _ from 'lodash'
export const lockerState = {
locker: {
list_01: [],
list_02: [],
list_03: [],
list_04: [],
list_06: []
}
}
export const lockerAction = {
INIT_LIST: 'INIT_LIST',
}
// REDUCERS
export const lockerReducer = (state = lockerState, action) => {
switch (action.type) {
case lockerAction.INIT_LIST:
switch (action.payload.menu) {
case '01':
console.log(action.payload.menu, action.payload.list, action.payload.menu)
return {
...state,
list_01: [state.locker.list_01, action.payload.list]
}
case '02':
return {
...state,
list_02: [state.locker.list_02, action.payload.list]
}
case '03':
return {
...state,
list_03: [state.locker.list_03, action.payload.list]
}
case '04':
return {
...state,
list_04: [state.locker.list_04, action.payload.list]
}
case '06':
return {
...state,
list_06: [state.locker.list_06, action.payload.list]
}
default:
return {
...state,
list_01: [...state.locker.list_01, action.payload.list]
}
}
default:
return state
}
}
// ACTION CREATOR
export const initList = (category, list, menu) => {
//console.log('initList action creator-> ', category, list, menu)
return {
type: lockerAction.INIT_LIST,
payload: {
category, list, menu
}
}
}
及其替代词std::is_pod
,@ JosephMansfield的答案摘录为:
std::is_trivial && std::is_standard_layout
请注意,#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");
}
仍然是标准布局。
答案 4 :(得分:1)
我有一个例子来说明不同之处:
#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 + Value initialization
cout << x << endl;
return 0;
}
输出:
5
0
我们可以看到对空A()构造函数的调用不会初始化成员,而对B()可以初始化成员。
答案 5 :(得分:0)
通过 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
在 gcc 的情况下为 0)。在这种情况下这显然是不希望的,定义一个空的构造函数 T() {}
会阻止这种情况。
事实上,当 CLion 建议将 T() {}
替换为 T() = default
时,我曾经遇到过这种情况。这导致了显着的性能下降和数小时的调试/基准测试。
所以我毕竟更喜欢使用空构造函数,除非我真的希望能够使用聚合初始化。