这是来自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
,当test
超出范围时,在函数act1
结束时第二次。我在哪里弄错了?
为什么需要finally
功能?我不能定义Final_action act1([&]{delete p;cout<<"Goodbye,cruel world\n"})
吗?这是一样的吗?
此外,如果有人能想到更好的标题,请修改当前标题。
更新:在进一步思考之后,我现在确信析构函数可以被调用三次。另外一个是用于调用函数void test()
中的临时对象自动生成,用作act1
的复制构造函数的参数。这可以使用g ++中的-fno-elide-constructors
选项进行验证。对于那些与我有同样问题的人,请参阅Bill Lynch的答案中指出的Copy elision和Return value optimization。
答案 0 :(得分:13)
你是对的,这段代码坏了。它仅在应用return value optimizations时才能正常工作。这一行:
auto act1 = finally([&]{delete p;cout<<"Goodbye,cruel world\n"})
可能会也可能不会调用复制构造函数。如果是,那么你将有两个Final_action
类型的对象,因此你将两次调用该lambda。
答案 1 :(得分:5)
最简单的解决方法是
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
来延长其生命周期 - 也没有任何复制或移动。
答案 2 :(得分:3)
代码被破坏了。 SimonKraemer提到的修订后的代码也坏了 - 它没有编译(finally
中的return语句是非法的,因为Final_action
既不可复制也不可移动)。仅使用生成的移动构造函数进行Final_action
移动也不起作用,因为F
保证有移动构造函数(如果它没有&#39; t,那么Final_action
&# 39;生成的移动构造函数将默默使用F
的复制构造函数作为后备),移动后F
也不保证是无操作。事实上,示例中的lambda将 not 变成no-op。
有一个相对简单易用的解决方案:
向bool valid = true;
添加标记Final_action
并覆盖移动c&#39并移动分配以清除源对象中的标记。仅在clean()
时调用valid
。这可以防止生成复制文件和复制分配,因此不需要显式删除它们。 (奖励积分:将旗帜放入可重复使用的仅限移动包装中,这样您就不必实施移动操作并移动Final_action
的分配。您不需要在这种情况下显式删除。)
或者,移除Final_action
的模板参数,然后将其更改为使用std::function<void()>
。在调用之前检查clean
是否为空。添加移动设置并移动将原始std::function
设置为nullptr
的分配。 (是的,这是必要的可移植。移动std::function
不保证源是空的。)优点:类型擦除的常见好处,例如能够返回范围保护到外部堆栈帧而不暴露F
。缺点:可能会增加大量的运行时间开销。
在我目前的工作项目中,我基本上使用类型擦除函数对象将这两种方法与ScopeGuard<F>
和AnyScopeGuard
结合起来。前者使用boost::optional<F>
并可以转换为后者。作为允许范围保护为空的额外好处,我也可以明确dismiss()
它们。这允许使用范围保护设置事务的回滚部分,然后在提交时解除它(使用非抛出代码)。
更新:Stroustrup的新例子甚至无法编译。我错过了明确删除副本c&#39; tor也禁用了移动c的生成。