“=default”和“{}”在默认构造函数和析构函数中有何不同?

247

我最初将这个问题仅限于析构函数,但现在我要考虑默认构造函数。以下是原始问题:

如果我想给我的类一个虚拟的析构函数,但与编译器生成的相同,我可以使用=default

class Widget {
public:
   virtual ~Widget() = default;
};

但是使用一个空的定义似乎可以用更少的打字实现相同的效果:

class Widget {
public:
   virtual ~Widget() {}
};

这两个定义方式有什么不同的行为吗?根据对这个问题的回答,对于默认构造函数的情况似乎也是类似的。既然"=default"和"{}"对于析构函数的意义几乎没有区别,那么对于默认构造函数这些选项的意义是否也相差无几呢?也就是说,假设我想创建一个类型,该类型的对象将被创建和销毁,为什么我要使用:

Widget() = default;

代替

Widget() {}

如果在发布问题后继续扩展问题违反了 Stack Overflow 的某些规则,我深表歉意。 对于默认构造函数发布几乎相同的问题,我认为这是不太理想的选择。


3
据我所知没有,但是在我看来= default更加明确,并且与对构造函数的支持保持一致。 - chris
14
一个虚析构函数从不是平凡的。 - Luc Danton
相关:https://dev59.com/LmEi5IYBdhLWcg3w6f6e - Gabriel Staples
3个回答

154

当涉及构造函数和析构函数时,这是完全不同的问题。

如果你的析构函数是virtual,那么差异可以忽略,正如Howard所指出的那样。然而,如果你的析构函数是非虚拟的,那就完全不同了。构造函数也是如此。

使用= default语法来定义特殊成员函数(默认构造函数、复制/移动构造函数/赋值函数、析构函数等)与仅使用{}是完全不同的。后者使该函数变为“用户提供的”。这改变了一切。

按照C++11的定义,这是一个微不足道的类:

struct Trivial
{
  int foo;
};

如果您尝试默认构造一个对象,编译器会自动生成一个默认构造函数。同样的道理也适用于复制/移动和析构操作。因为用户没有提供这些成员函数中的任何一个,所以C++11规范将其视为“平凡”类。因此,像使用memcpy函数来初始化它们一样,可以对其内容进行复制并进行其他操作。
这段话的意思是:如果你没有提供默认构造函数、拷贝构造函数、移动构造函数和析构函数,编译器会自动生成这些函数。C++11规定了这种情况下的类被称为“平凡类”,可以对其内容进行复制和其他操作。
struct NotTrivial
{
  int foo;

  NotTrivial() {}
};

顾名思义,这不再是琐碎的。它有一个用户提供的默认构造函数。即使它是空的,根据C++11的规则,它也不能是琐碎类型。

这个:

struct Trivial2
{
  int foo;

  Trivial2() = default;
};

作为名称所示,这是一种琐碎的类型。为什么?因为你告诉编译器自动生成默认构造函数。因此,构造函数不是“用户提供的”。因此,该类型被视为琐碎的,因为它没有用户提供的默认构造函数。
“= default”语法主要用于执行复制构造函数/赋值时,当你添加成员函数以防止创建这些函数时。但它还会触发编译器的特殊行为,因此在默认构造函数/析构函数中也很有用。

5
因此,关键问题似乎是生成的类是否平凡。潜在的问题是特殊函数是用户声明(这适用于=default函数)还是用户提供(这适用于{})函数之间的区别。用户声明和用户提供的函数都可以防止生成其他特殊成员函数(例如,用户声明的析构函数会防止生成移动操作),但只有用户提供的特殊函数才会使类变得不平凡。没错吧? - KnowItAllWannabe
@知道一切的菜鸟:是的,那就是大致的想法。 - Nicol Bolas
这里似乎缺少一个单词:“就C++11规则而言,您拥有平凡类型的权利。”我想修复它,但我不确定原意是什么。 - jcoder
7
=default可用于强制编译器生成默认构造函数,即使存在其他构造函数;如果已提供任何其他用户声明的构造函数,则不会隐式声明默认构造函数。 - bgfvdu3w
@KnowItAllWannabe 这是否意味着(针对您的第一条评论),用户声明的(即=default)可能会阻止自动生成复制构造函数,例如? - LCsa
显示剩余2条评论

54
重要的区别在于,
class B {
    public:
    B(){}
    int i;
    int j;
};

并且

class B {
    public:
    B() = default;
    int i;
    int j;
};

默认构造函数定义为B() = default;被认为是非用户定义的。这意味着,在值初始化的情况下,如下:

B* pb = new B();  // use of () triggers value-initialization

一种特殊类型的初始化不使用构造函数,对于内置类型,这将导致零初始化。在 `B() {}` 的情况下,这种初始化不会发生。C++ 标准 n3337 § 8.5/7 规定:
对于类型 T 的值初始化意味着: - 如果 T 是一个(可能带有 cv 限定符的)类类型(第 9 条),并且具有用户提供的构造函数(12.1),则调用 T 的默认构造函数(如果 T 没有可访问的默认构造函数,则初始化是非法的); - 如果 T 是一个(可能带有 cv 限定符的)非联合类类型,并且没有用户提供的构造函数,则对象将被零初始化,并且如果 T 的隐式声明的默认构造函数是非平凡的,则调用该构造函数。 - 如果 T 是数组类型,则每个元素都将被值初始化;否则,对象将被零初始化。
例如:
#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

可能的结果:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd


那么,为什么“{}”和“= default”总是初始化std::string http://ideone.com/LMv5Uf? - nawfel bgh
3
@nawfelbgh 默认构造函数A(){}调用std::string的默认构造函数,因为它是非POD类型。std::string的默认构造函数将其初始化为空的0大小字符串。标量的默认构造函数不执行任何操作:具有自动存储期限的对象(及其子对象)将被初始化为不确定的值。 - 4pie0
3
仅为了清楚起见,上面的示例中,类B始终产生值0: https://ideone.com/XOcHNq A:0,0 B:0,0 A:145084416,0 B:0,0 A:145084432,0 B:0,0 A:145084416,0 //... - JSoet

50

它们都不是微不足道的。

根据基类和成员的 noexcept 说明,它们都具有相同的 noexcept 说明。

到目前为止我发现唯一的区别是,如果Widget包含一个具有不可访问或已删除析构函数的基类或成员:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

那么使用=default方案将会编译通过,但是Widget不会是一个可销毁的类型。也就是说,如果你试图销毁一个Widget,会得到一个编译时错误。但是如果不这样做,你将拥有一个工作正常的程序。

另一方面,如果提供了用户自定义的析构函数,那么无论是否销毁Widget,程序都无法编译通过:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.

12
有趣的是,换句话说,使用=default;时,编译器只有在需要生成析构函数时才会生成它,因此不会触发错误。这个行为对我来说很奇怪,尽管并不一定是错误。我无法想象标准中 强制规定 这种行为。 - Nik Bougalis
2
那么 =default 解决方案将会编译吗?不会的。我刚在 VS 中测试过了。 - Minimus Heximus
2
错误信息是什么,使用的是哪个版本的VS? - Howard Hinnant
如果我没记错的话,你需要在堆上分配对象才能进行有效的测试 - 声明一个本地的 Widget 会自动尝试销毁它,所以它不会编译通过,但是写 new Widget=default 的情况下可以工作,只要你从不 delete 它。 - celticminstrel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接