[英]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 world\n";} );
}
我有两个问题:
根据作者的说法, delete p
只被调用一次:当act1超出范围时。 但是从我的理解:首先, act1
将用复制构造函数初始化,然后函数中的临时对象Final_action<F>(f)
finally
被破坏,第一次调用delete p
,然后在第二次调用delete p
当act1
超出范围时的函数test
。 我哪里弄错了?
为什么需要finally
函数? 我不能只定义Final_action act1([&]{delete p;cout<<"Goodbye,cruel world\\n"})
? 那是一样的吗?
此外,如果有人能想到更好的标题,请修改当前的标题。
更新:在进一步思考之后,我现在确信析构函数可能会被调用三次。 另外一个用于调用函数void test()
的临时对象自动生成,用作act1
的复制构造函数的act1
。 这可以使用g ++中的-fno-elide-constructors
选项进行验证。 对于那些和我有同样问题的人,请参阅Bill Lynch的答案中指出的复制省略以及返回值优化 。
你是对的,这段代码坏了。 它仅在应用返回值优化时才能正常工作。 这一行:
auto act1 = finally([&]{delete p;cout<<"Goodbye,cruel world\n"})
可能会也可能不会调用复制构造函数。 如果是,那么你将有两个类型为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 world\n";} );
使用copy-list-initialization和生命周期扩展转发引用可以避免对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
并覆盖移动c'tor并移动赋值以清除源对象中的标志。 仅在valid
调用clean()
。 这可以防止生成复制c'tor和复制分配,因此不需要显式删除它们。 (加分点:将标志放入可重复使用的仅移动包装器中,这样您就不必实现移动控制并移动Final_action
赋值。在这种情况下,您也不需要显式删除。)
或者,删除Final_action
的模板参数并将其更改为使用std::function<void()>
。 在调用clean
之前检查clean
是否为空。 添加move c'tor并移动将原始std::function
为nullptr
赋值。 (是的,这是必要的可移植。移动std::function
并不能保证源是空的。)优点:类型擦除的常见好处,例如能够将示波器防护装置返回到外部堆栈框架中没有暴露F
缺点:可能会增加大量的运行时间开销。
在我目前的工作项目中,我基本上ScopeGuard<F>
两种方法与ScopeGuard<F>
和使用类型擦除功能对象的AnyScopeGuard
相结合。 前者使用boost::optional<F>
,可以转换为后者。 作为允许范围保护为空的额外好处,我也可以明确地dismiss()
它们。 这允许使用范围保护设置事务的回滚部分,然后在提交时解除它(使用非抛出代码)。
更新:Stroustrup的新示例甚至不编译。 我错过了明确删除副本c'tor也禁用移动c'tor的生成。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.