如何为C++中函数返回的临时对象调用析构函数
How is destructor called for temporary objects returned from a function in C++?
这是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";} );
}
我有两个问题:
根据作者的说法,
delete p
只被调用一次:当act1超出范围时。但据我所知:首先,act1
将用复制构造函数初始化,然后函数finally
中的临时对象Final_action<F>(f)
将被销毁,第一次调用delete p
,第二次在函数test
结束时,当act1
超出范围时。我哪里搞错了?为什么需要
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的生成。
- C++:允许临时对象调用非常量成员函数的设计理念是什么?
- 在函数调用中C++临时对象的生存期
- 如果类没有任何成员变量,则通过临时对象调用类的成员函数的开销是多少?
- 为什么我不能直接在临时对象上调用 operator()?
- C 向构造函数和临时对象的明确调用
- 何时在函数调用中发生临时对象的破坏
- 使用临时对象调用构造函数
- 为什么在传递临时对象时不调用复制构造函数
- 何时调用使用"new"创建的临时对象'delete'?
- 将临时对象分配给调用对象
- 为什么不从临时对象中调用move ctor进行构造(运算符+的结果)
- 理解临时对象上下文中的函数调用
- 使用默认构造函数返回临时对象时,Destuctor调用了两次
- 临时对象的生命周期:嵌套函数调用中指向临时向量的迭代器
- 有多少种方法可以生成临时对象/不必要地调用构造函数
- 为什么不调用复制构造函数将临时对象复制到新定义的对象
- 当有额外的括号时,在临时对象上调用用户定义的操作符+出错
- 如何为C++中函数返回的临时对象调用析构函数
- 我可以直接调用operator()而不创建临时对象吗?
- 在调用点参数处创建的临时对象的生命周期