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

vojdkbi0  于 2023-04-01  发布在  其他
关注(0)|答案(7)|浏览(219)

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

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

为什么不直接说:

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

为什么要引入新的语法呢

von4xj4u

von4xj4u1#

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

  • §12.1/6 [class.ctor]* 默认且未定义为已删除的默认构造函数是隐式定义的,当它被odr-用于创建其类类型的对象时,或者当它在其第一次声明后显式默认时。隐式定义的默认构造函数执行类的初始化集合,该初始化集合将由用户编写的默认构造函数为该类执行,而无需ctor-初始化器(12.6.2)和一个空的复合语句。[...]

然而,虽然两个构造函数的行为相同,但提供一个空的实现确实会影响类的一些属性。给出一个用户定义的构造函数,即使它什么也不做,也会使类型不是 aggregate,也不是 trivial。如果你想让你的类是一个aggregate或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确实带来了一些统一性,因为它也可以与复制/移动构造函数和析构函数一起使用。不会与默认复制构造函数执行相同的操作(它将执行其成员的成员级复制)。(或= delete)语法,通过显式地说明意图,使代码更易于阅读。

velaa5lx

velaa5lx2#

我有一个例子可以说明其中的区别:

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

lskq00tm

lskq00tm3#

n2210提供了一些原因:
违约管理存在几个问题:

  • 构造函数定义是耦合的;声明任何构造函数都会抑制默认构造函数。
  • 析构函数默认值不适用于多态类,需要显式定义。
  • 一旦默认被抑制,就没有办法恢复它。
  • 默认实现通常比手动指定的实现更有效。
  • 非默认实现是非平凡的,这会影响类型语义,例如使类型成为非POD。
  • 如果不声明一个(非平凡的)替代,就无法禁止一个特殊的成员函数或全局运算符。
type::type() = default;
type::type() { x = 3; }

在某些情况下,类主体可以在不需要更改成员函数定义的情况下更改,因为默认值会随其他成员的声明而更改。
参见Rule-of-Three becomes Rule-of-Five with C++11?
请注意,对于显式声明任何其他特殊成员函数的类,不会生成移动构造函数和移动赋值运算符,对于显式声明移动构造函数或移动赋值运算符的类,不会生成复制构造函数和复制赋值运算符,并且具有显式声明的析构函数和隐式定义的复制构造函数或隐式定义的复制赋值运算符的类认为已弃用

2q5ifsrm

2q5ifsrm4#

在某些情况下,这是一个语义问题。对于默认构造函数来说,这不是很明显,但对于其他编译器生成的成员函数来说,这就变得很明显了。
对于默认构造函数,可以将任何具有空主体的默认构造函数视为普通构造函数的候选者,就像使用=default一样。毕竟,旧的空默认构造函数是 * 法律的的C++*。

struct S { 
  int a; 
  S() {} // legal C++ 
};

编译器是否理解这个构造函数是无关紧要的,在大多数情况下,除了优化(手动或编译器)。
然而,这种将空函数体视为“默认”的尝试对于其他类型的成员函数完全无效。考虑复制构造函数:

struct S { 
  int a; 
  S() {}
  S(const S&) {} // legal, but semantically wrong
};

在上面的例子中,使用空body编写的复制构造函数现在是 * 错误的 *。它不再实际复制任何东西。这是一组与默认复制构造函数语义非常不同的语义。所需的行为需要您编写一些代码:

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这样简单的类型,所需要的代码已经比您所希望的要多了(特别是当我们把move操作抛进mix中时).我们不能依赖一个空的body来表示“填充默认值”,因为空的body已经是完全有效的,并且有一个明确的含义。如果空的主体被用来表示“填充默认值”,那么就没有办法显式地创建无操作复制构造函数等。
这又是一个一致性的问题。空的主体意味着“什么都不做”,但是对于像复制构造函数这样的东西,你真的不想“什么都不做”,而是“做所有你在没有被抑制的情况下通常会做的事情”。因此,=default。它是克服被抑制的编译器生成的成员函数(如复制/移动构造函数和赋值运算符)的必要条件。然后它就“显而易见”了使其也适用于默认构造函数。
如果只是为了在某些情况下使旧代码更优化,那么让默认构造函数具有空的主体和平凡的成员/基构造函数也被认为是平凡的,就像它们在=default中一样,这可能是很好的。但是大多数依赖于平凡的默认构造函数进行优化的低级代码也依赖于平凡的复制构造函数。如果你必须去“修复”所有旧的复制构造函数,修复所有旧的默认构造函数也不是什么难事,使用一个显式的=default来表示你的意图也更清晰和明显。
编译器生成的成员函数还可以做一些其他的事情,你必须显式地进行更改才能支持这些事情,支持constexpr的默认构造函数就是一个例子。使用=default比使用=default暗示的所有其他特殊关键字标记函数更容易,这是C11的主题之一:它仍然有很多缺点和缺陷,但很明显,在易用性方面,它比C03前进了一大步。

zf9nrax1

zf9nrax15#

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

bweufnob

bweufnob6#

通过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() = default替换T() {}时,它导致了显着的性能下降和数小时的调试/基准测试。
所以我还是更喜欢使用空的构造函数,除非我真的希望能够使用聚合初始化。

drnojrws

drnojrws7#

澄清trozen的answer和Slavenskij的answer所指出的行为将是很好的。
Widget() {};这样的空默认构造函数被视为用户自定义的默认构造函数,而Widget() = default;则不是。这导致在前一种情况下默认初始化,而在后者中值初始化,在涉及Widget w = new Widget()Widget w{}等形式的定义中。
更多细节请参见here,您还将了解到= default;的位置对于获得编译器生成的默认构造函数很重要。

相关问题