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

时间:2015-09-17 14:14:23

标签: c++ raii

这是来自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";} );
}

我有两个问题:

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

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

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

    更新:在进一步思考之后,我现在确信析构函数可以被调用三次。另外一个是用于调用函数void test()中的临时对象自动生成,用作act1的复制构造函数的参数。这可以使用g ++中的-fno-elide-constructors选项进行验证。对于那些与我有同样问题的人,请参阅Bill Lynch的答案中指出的Copy elisionReturn value optimization

3 个答案:

答案 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的生成。