如何为C++中函数返回的临时对象调用析构函数

How is destructor called for temporary objects returned from a function in C++?

本文关键字:临时对象 调用 析构函数 返回 函数 C++      更新时间:2023-10-16

这是Stroustrup的"C++编程语言"中的一段代码,它实现了一个finally,我无法完全理解在哪里调用析构函数。

template<typename F> struct Final_action
{
  Final_action(F f): clean{f} {}
  ~Final_action() { clean(); }
  F clean;
}
template<class F> 
Final_action<F> finally(F f)
{
  return Final_action<F>(f);
}
void test(){
  int* p=new int{7};
  auto act1 = finally( [&]{delete p;cout<<"Goodbye,cruel worldn";} );
}

我有两个问题:

  1. 根据作者的说法,delete p只被调用一次:当act1超出范围时。但据我所知:首先,act1将用复制构造函数初始化,然后函数finally中的临时对象Final_action<F>(f)将被销毁,第一次调用delete p,第二次在函数test结束时,当act1超出范围时。我哪里搞错了?

  2. 为什么需要finally功能?我不能只定义Final_action act1([&]{delete p;cout<<"Goodbye,cruel worldn"})吗?是一样的吗?

此外,如果有人能想出更好的标题,请修改当前的标题。

更新:经过进一步思考,我现在确信析构函数可能会被调用三次。另外一个是在调用函数void test()中自动生成的临时对象,用作act1的复制构造函数的参数。这可以通过g++中的-fno-elide-constructors选项进行验证。对于那些和我有同样问题的人,请参阅Bill Lynch在回答中指出的复制省略以及返回值优化。

您是对的,此代码已损坏。只有在应用返回值优化时,它才能正常工作。此行:

auto act1 = finally([&]{delete p;cout<<"Goodbye,cruel worldn"})

可以调用也可以不调用复制构造函数。如果是,那么您将有两个类型为Final_action的对象,因此您将调用该lambda两次。

最简单的修复方法是

template<typename F> 
struct Final_action
{
  Final_action(F f): clean{std::move(f)} {}
  Final_action(const Final_action&) = delete;
  void operator=(const Final_action&) = delete;
  ~Final_action() { clean(); }
  F clean;
};
template<class F> 
Final_action<F> finally(F f)
{
  return { std::move(f) };
}

并用作

auto&& act1 = finally( [&]{delete p;cout<<"Goodbye,cruel worldn";} );

使用复制列表初始化和寿命延长转发引用可以避免Final_action对象的任何复制/移动。复制列表初始化直接构造临时Final_action返回值,finally返回的临时通过绑定到act1来延长其生存期,也不需要任何复制或移动。

代码已损坏。SimonKraemer提到的修订代码已损坏–它不编译(finally中的return语句是非法的,因为Final_action既不可复制也不可移动(。仅使用生成的移动构造函数使Final_action移动也不起作用,因为F保证有移动构造函数(如果没有,则Final_action的生成移动构造函数将静默地使用F的复制构造函数作为后备(,也不保证F在移动后是无操作的。事实上,示例中的lambda不会变成no-op。

有一个相对简单和便携的解决方案:

将标志bool valid = true;添加到Final_action,并覆盖move c'tor和move assignment以清除源对象中的标志。如果valid,则仅调用clean()。这样可以防止生成复制c'tor和复制赋值,因此不需要显式删除它们。(加分:将标志放入可重复使用的仅移动包装器中,这样您就不必实现Final_action的移动c'tor和移动赋值。在这种情况下,您也不需要显式删除。(

或者,删除Final_action的模板参数,并将其更改为使用std::function<void()>。在调用clean之前,请检查它是否为空。添加将原始std::function设置为nullptr的move c'tor和move assign。(是的,这是可移植的必要条件。移动std::function并不能保证源为空。(优点:类型擦除的常见好处,例如可以将作用域保护返回到外部堆栈帧中,而不会暴露F。缺点:可能会增加大量的运行时间开销。

在我目前的工作项目中,我基本上将这两种方法与使用类型擦除函数对象的ScopeGuard<F>AnyScopeGuard相结合。前者使用boost::optional<F>,并且可以转换为后者。作为允许作用域保护为空的额外好处,我也可以显式地dismiss()它们。这允许使用范围保护来设置事务的回滚部分,然后在提交时将其驳回(使用非抛出代码(。

更新:Stroustrup的新示例甚至没有编译。我忽略了明确删除复制c'tor也会禁用移动c'tor的生成。