为什么临时的生存期延长会导致析构函数被多次调用



考虑以下片段:

#include <iostream>
using namespace std;
class Temp {
public:
Temp() { cout << "Temp()" << endl;}
~Temp() { cout << "~Temp()" << endl;}
};
Temp GetTemp() {
cout << "GetTemp" << endl;
return Temp();
}
Temp TakeTemp(Temp temp) {
cout << "TakeTemp" << endl;
return temp;
}

int main()
{
TakeTemp(GetTemp());
return 0;
}

当我运行TakeTemp(GetTemp());时,输出看起来像

GetTemp                                                                                                                                                        
Temp()                                                                                                                                                         
TakeTemp                                                                                                                                                       
~Temp()                                                                                                                                                        
~Temp()     

注意,这里~Temp()被调用了两次(但只构造了一个临时对象)。这似乎很奇怪,因为1)GetTemp()返回的临时变量的生存期应该延长到完整的表达式,2)由于我们在TakeTemp中直接返回temp,返回值optimization将重用同一对象。

有人能解释为什么这里有多个dstor电话吗?

(注意,如果我们放置更多的TakeTemp()层,dstor调用的数量会成比例增长。)

函数TakeTemp按值获取参数,并按值返回参数。

您正在那里进行复制,因此现在有两个Temp对象要删除。

您看到的两个被破坏的对象是这里调用的两个函数的返回值:

TakeTemp(GetTemp());
^ returns a Temp
^ returns a Temp

使用C++17术语,这两个对象是:

  1. 函数参数Temp temp;
  2. TakeTemp的返回值

函数调用GetTemp()是一个prvalue。由于它是函数调用的参数,因此其结果对象是匹配的参数Temp temp临时物化转换应用于Temp temp的构建点。

请注意,GetTemp()函数内部没有临时创建。语句return Temp();并不意味着要创建一个对象;它给出了稍后将用于最终创建对象的参数。在实现prvalue之前,不会创建任何对象。

然后,执行return temp;创建第二个对象。这与return Temp();不同,因为temp是一个左值,而不是一个prvalue。函数调用TakeTemp的返回值对象是使用temp作为初始值设定项创建的。这不是一个复制省略上下文。如果您向Temp添加一个复制构造函数,您将看到该对象的复制构造消息。

概括一下,事件的顺序是:

  • GetTemp主体已输入
  • 执行GetTemp返回语句,初始化参数Temp temp
  • GetTemp主体退出(销毁任何局部变量(如果有的话))
  • 输入TakeTemp正文
  • 执行TakeTemp返回语句,初始化TakeTemp的返回值对象
  • TakeTemp主体退出(执行返回到main)
  • {参数Temp temp已销毁
  • TakeTemp的返回值对象已销毁

Temp temp的寿命是函数参数的寿命;它在函数返回后被销毁。

TakeTemp的返回值的生存期是一个临时对象生存期,因此它一直持续到完整表达式结束。

请注意,函数参数生存期有一个怪癖:它是由实现定义的,无论是在调用后立即销毁,还是在完整表达式结束时销毁。因此,我上面列表中的最后两个步骤可以按任意顺序进行。在我安装g++8.2.1时,函数参数实际上是两个析构函数中较晚的一个。

最新更新