新的关键字=默认在C + + 11
我不明白为什么我会这样做:
struct S { int a; S(int aa) : a(aa) {} S() = default; };
为什么不直接说:
S() {} // instead of S() = default;
为什么带来一个新的关键字?
默认的默认构造函数被定义为与没有初始化列表和空的复合语句的用户定义的默认构造函数相同。
§12.1/ 6 [class.ctor]默认的默认构造函数是默认的,当它被odr用来创build它的类types的对象时,或者当它在第一次声明后被明确地默认时,被隐式地定义。 隐式定义的默认构造函数执行该类的初始化设置,该类将由用户编写的该类的默认构造函数执行,该类没有ctor-initializer(12.6.2)和空的复合语句。 […]
但是,虽然两个构造函数的行为都是相同的,但提供一个空的实现会影响类的某些属性。 给一个用户定义的构造函数,尽pipe它什么都不做,使得types不是一个聚合 ,也不是微不足道的 。 如果你想让你的类是一个聚合类或者一个普通类(或者是一个传递性,一个PODtypes),那么你需要使用= 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"); }
此外,显式默认构造函数将使得它隐含的构造函数将会并且也会给它隐含的构造函数所具有的相同的exception说明。 在这种情况下,隐含的构造函数不会被constexpr
(因为它会使数据成员未初始化),它也会有一个空的exception规范,所以没有区别。 但是,在一般情况下,您可以手动指定constexpr
和exception规范来匹配隐式构造函数。
使用= default
会带来一些一致性,因为它也可以用于复制/移动构造函数和析构函数。 例如,一个空的拷贝构造函数不会像默认的拷贝构造函数(它将执行其成员的成员拷贝)一样。 对这些特殊成员函数中的每一个均匀地使用= default
(或= delete
)语法,通过明确地说明你的意图使代码更容易阅读。
n2210提供了一些原因:
违约pipe理有几个问题:
- 构造函数的定义是耦合的; 声明任何构造函数抑制默认的构造函数。
- 析构函数的默认值对多态类是不合适的,需要明确的定义。
- 一旦违约被压制,就没有办法重新生效。
- 默认的实现通常比手动指定的实现更高效。
- 非默认实现是非平凡的,它会影响types的语义,例如使一个types非POD。
- 没有声明一个(非平凡的)替代品,就没有办法禁止特殊的成员职能或全球运营者。
type::type() = default; type::type() { x = 3; }
在某些情况下,类的主体可以在不需要更改成员函数定义的情况下进行更改,因为默认情况下会随附加成员的声明而更改。
请参阅三规则与C ++ 11成为五规则? :
请注意,移动构造函数和移动赋值运算符不会为显式声明任何其他特殊成员函数的类生成,将不会为显式声明移动构造函数或移动的类生成复制构造函数和复制赋值运算符赋值运算符,并且具有显式声明的析构函数和隐式定义的复制构造函数或隐式定义的复制赋值运算符的类被认为是不赞成的
在某些情况下,这是一个语义问题。 这对于默认的构造函数来说并不是很明显,但是对于其他编译器生成的成员函数来说,这一点显而易见
对于默认构造函数,可以使任何具有空体的默认构造函数被认为是一个简单的构造函数,与使用=default
相同。 毕竟,旧的空的默认构造函数是合法的C ++ 。
struct S { int a; S() {} // legal C++ };
不pipe编译器是否理解这个构造函数都是微不足道的,在大多数情况下(手动编译器或编译器)是无关紧要的。
但是,这种将空函数体视为“默认”的尝试完全针对其他types的成员函数。 考虑复制构造函数:
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
,基本上)。 编译器将不得不检查每个成员的初始化expression式,并确保它与expression式相同,以访问源对应的成员,而不是别的,确保没有成员留下不平凡的默认构造,等等。它是在过程的方式向后编译器会用来validation它自己生成的这个函数的版本是微不足道的。
那么考虑一下复制赋值运算符,这个赋值运算符可能会更加复杂,特别是在非平凡的情况下。 这是一大堆锅炉,你不想为许多课程写作,但是你仍然被迫在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
这样一个简单的types编写了代码(特别是一旦我们将混合的操作转移到其中)。 我们不能依靠一个空的身体来“填补默认”,因为空的身体已经是完全有效的,并且有明确的意义。 事实上,如果用空表示“填入默认值”,那么就没有办法明确地做一个没有操作的拷贝构造函数或类似的东西。
这又是一个问题。 空洞的意思是“什么也不做”,但是对于复制构造者来说,你真的不希望“无所事事”,而是“如果不加以压制,就要做所有你通常会做的事情”。 因此=default
。 有必要克服压缩编译器生成的成员函数,如复制/移动构造函数和赋值运算符。 那么,使它对于默认的构造函数也是“明显的”。
如果只是为了在某些情况下使旧的代码更优化,而使用空的主体或者普通的成员/基础构造函数也可能被认为是微不足道的,在简单的默认构造函数进行优化也依赖于简单的复制构造函数。 如果你将不得不去修复你所有的旧的构造函数,那么修复你所有的旧的构造函数也不是什么难事。 用明确的=default
来表示你的意图也更清楚和更明显。
还有一些编译器生成的成员函数会做的其他事情,你也必须明确地进行修改以支持。 默认构造函数支持constexpr
就是一个例子。 在使用=default
,使用=default
更容易,而不是像所有其他特殊关键字那样标记函数, =default
隐含这些=default
,这是C ++ 11的主题之一:使语言更容易。 它仍然有大量的瑕疵和背后的妥协,但很明显,这是C ++ 03向易用性迈出的一大步。