以下代码是在Visual Studio 2012 Express for Windows Desktop中编译和运行的,作为学习练习。
#include <cstdio>
class X
{
public:
X() { printf("default constructed\n"); }
~X() { printf("destructed\n");}
X(const X&) { printf("copy constructed\n"); }
X(X&&) { printf("move constructed\n"); }
X & operator= (const X &) { printf("copy assignment operator\n"); }
};
X A() {
X x;
return x;
}
int main() {
{
A();
}
std::getchar();
}
在禁用编译器优化(/ Od)的情况下编译时,结果输出表明析构函数被调用两次。考虑到只构造了一个对象,这是一个问题。 为什么析构函数被调用两次?如果类管理自己的资源,这不是问题吗?
default constructed
move constructed
destructed
destructed <<< Unexpected call
我尝试了几个实验来尝试解释输出,但最终这些并未导致任何有用的解释。
实验1:在使用优化(/ O1或/ O2)编译相同代码时,结果输出为:
default constructed
destructed
表示命名返回值优化已经删除了对移动构造函数的调用,并掩盖了底层问题。
实验2:禁用优化并注释掉移动构造函数。产生的输出是我的预期。
default constructed
copy constructed
destructed
destructed
答案 0 :(得分:7)
请记住,当一个对象是移动操作的源时,它仍然会被销毁。因此,移动的来源需要将自己置于一种状态,即被破坏的状态不会释放它不再拥有的资源(因为它们被移动到另一个对象)。例如,源对象中的任何原始指针(现在将由移动构造对象拥有)应设置为NULL。
答案 1 :(得分:3)
A中的X在超出范围时被销毁。
A返回一个临时对象(由移动构造函数从X构造),它是一个单独的实例。这在调用者的范围内被破坏。这将导致再次调用析构函数(在临时)。
选择移动构造函数是因为编译器检测到X将在之后立即销毁。要使用此方法,移动构造函数应该使原始对象中的任何数据无效或重置,以便析构函数不会使移动目标所接管的任何数据无效。
当您按值传递rvalue,或者从函数返回任何值时,编译器首先会获得删除副本的选项。如果副本没有被删除,但是有问题的类型有移动构造函数,则编译器需要使用移动构造函数。
http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references/
当您从创建临时对象的作用域退出时,它将被销毁。如果引用绑定到临时对象,则当引用超出范围时,将销毁临时对象,除非它先前被控制流中断销毁。
RVO可以产生与非优化版本不同的行为:
返回值优化,或简称为RVO,是一种编译器优化技术,它涉及消除为保存函数返回值而创建的临时对象。[1]在C ++中,特别值得注意的是允许更改生成的程序的可观察行为。[2]
答案 2 :(得分:2)
虽然迈克尔和jspcal的答案都是准确的,但他们没有回答我的问题的核心,这就是为什么有两个析构函数调用。我只期待一个。
答案是函数A()返回一个临时对象。总是。这是函数返回值的工作方式,而移动语义与此事实无关。我猜迈克尔和jspcal认为我没有错过这样一个基本事实。我将术语“移动”等同于“交换”的概念。交换时,不会构造和销毁对象。因此我期待只有一个析构函数。
由于必须构造和销毁返回的对象,所以进行了第二次析构函数调用(以及第二次构造函数调用)。
现在,选择执行的实际构造函数取决于类定义中提供的内容。如果移动构造函数可用,则会调用它。否则将调用复制构造函数。