为什么在C++11中我们需要使用virtual ~A() = default;而不是virtual ~A() {}?

49
在 Stack Overflow 的帖子 Checking the object type in C++11 中,我有以下评论:

在 C++11 中,您实际上会想要执行 virtual ~A() = default;。否则,您将失去隐式移动构造函数。

virtual ~A() = default; 是什么?为什么使用 virtual ~A() {} 会丢失隐式移动构造函数?

13
+1是因为你成功导致了一万多名用户的错误答案。 - Lightness Races in Orbit
6
请注意,您始终可以使用相同的机制获得移动构造函数:A(A&&) = default; - Xeo
3个回答

42

这条评论不正确。

两者:

virtual ~A() = default;

virtual ~A() {}

如果析构函数是用户声明的,则禁止隐式移动成员。 用户声明的特殊成员在[dcl.fct.def.default] / p4中讨论了用户声明用户提供

如果一个特殊成员函数是用户声明的,且在其第一次声明时未明确默认或删除,则它就是用户提供的


1
@HowardHinnant:那么在这两种情况下,编译器都不会提供移动构造函数? - legends2k
@Pixelchemist:我认为你是对的。第8.4.2节“显式默认函数和隐式声明函数统称为默认函数,实现应为它们提供隐式定义。如果一个特殊成员函数在其第一次声明时是用户声明的且没有显式地默认或删除,则它是用户提供的。”这是来自n3337即C++11标准+1。 - legends2k
1
我认为默认函数的整个意义在于不抑制其他成员的生成,影响平凡性等。 - Puppy
1
@DeadMG:不,设置默认值的目的是在成员因某种原因被抑制时,可以避免大量样板文件。 - Xeo
1
@Xeo 还有其他原因需要非用户提供的函数。例如,~A() = default; 可能是一个微不足道的析构函数,但 ~A() {} 不行。 - bames53

27

在这篇文章中https://dev59.com/AnPYa4cB1Zd3GeqPj4Zk#17204598,我有一条评论:

在C++11中,您实际上要执行virtual ~A() = default;否则,您将失去隐式移动构造函数。

该评论是不正确的。

即使使用了default,那个析构函数也是“用户声明的”(尽管请注意它不是“用户提供的”)。

#include <iostream>

struct Helper
{
    Helper() {}
    Helper(const Helper& src) { std::cout << "copy\n"; }
    Helper(Helper&& src)      { std::cout << "move\n"; }
};

struct A
{
    virtual ~A() {}
    Helper h;
};

struct B
{
    virtual ~B() = default;
    Helper h;
};

struct C
{
    Helper h;
};


int main()
{
    {
        A x;
        A y(std::move(x));   // outputs "copy", because no move possible
    }

    {
        B x;
        B y(std::move(x));   // outputs "copy", because still no move possible
    }

    {
        C x;
        C y(std::move(x));   // outputs "move", because no user-declared dtor
    } 
}

实时演示

+ g++-4.8 -std=c++11 -O2 -Wall -pthread main.cpp
+ ./a.out
copy
copy
move

所以你并没有“丢失”任何东西 —— 最开始就没有移动功能!

这是标准中禁止在两种情况下隐式声明移动构造函数的规定:

[C++11: 12.8/9]: 如果类 X 的定义没有显式声明移动构造函数,当且仅当:

  • X 没有用户声明的复制构造函数,
  • X 没有用户声明的复制赋值运算符,
  • X 没有用户声明的移动赋值运算符,
  • X 没有用户声明的析构函数,并且
  • 移动构造函数不会被隐式定义为已删除。

附注

如果未来的标准能够明确列出“用户声明”的精确含义,那就更好了。至少有这么一段:

[C++11: 8.4.2/4]: [..] 如果一个特殊成员函数是用户声明的,并且在其第一次声明时没有显式指定为默认或删除,则它是用户提供的[..]

可以暗示出这里的区别。


1
虽然我不明白为什么标准不允许实现提供移动构造函数,即使用户没有提供它,也就是将其声明为未定义并执行= default;似乎是一样的。 - legends2k
1
@legends2k:我同意;理想情况下,12.8/9 可以改为“用户提供”,12.8/20 也是如此吧? - Lightness Races in Orbit
这些隐式特殊成员函数规则在C++11中变得更加复杂。什么意思是“在其第一次声明中显式地默认或删除”?第一次声明后是否可以有后续声明? - ThomasMcLeod
@ThomasMcLeod:定义也是声明,所以是的……除非您无法定义默认或删除的特殊成员函数。这里会有两个声明:struct A { ~A() = default; }; A::~A() {} 但是定义是非法的。我认为这可能只是笨拙或过于精确的措辞。 - Lightness Races in Orbit

5

这个评论是错误的。

如果你想让编译器提供一个移动构造函数,而不是提供自己的移动构造函数,那么其中一个要求就是它期望由编译器提供析构函数,也就是一个平凡的析构函数。然而,当前标准对于何时可以提供隐式实现非常严格——接受用户如何提供析构函数。任何由用户声明的内容都被认为是用户自己掌握的事情,因此不仅如此。

~A() { … }

但是还有这个。
~A() = default;

这个语法使得编译器不提供隐式析构函数。第一个是定义,因此也是声明;第二个只是声明。在这两种情况下,析构函数都是用户声明的,从而禁止编译器提供隐式移动构造函数。

我猜测背后的原因是,在移动期间,对象的资源被移动到另一个对象中,留下原始对象处于没有动态存储资源的状态;但如果你的类没有这样的资源,那么它可以轻松地移动、销毁等。当你声明一个非平凡的析构函数时,这提示编译器你管理的类的资源并不是一些简单的东西,你大多数情况下需要提供非平凡的移动,所以编译器不提供一个。


1
除非这不是该行为背后的原因,因为那就 不是 该行为。 - Lightness Races in Orbit
现在已经修复,以解决原帖作者的问题。 - legends2k

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