【问题标题】:How is destructor called for temporary objects returned from a function in C++?如何为从 C++ 中的函数返回的临时对象调用析构函数?
【发布时间】:2015-12-14 10:55:10
【问题描述】:

这是来自 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 将使用复制构造函数进行初始化,然后函数finally 中的临时对象Final_action&lt;F&gt;(f) 被破坏,第一次调用delete p,最后第二次调用当act1 超出范围时,函数test 的作用。我哪里弄错了?

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

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

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

【问题讨论】:

  • 每个问题一个问题,请。
  • re #2 我不明白为什么不这样做。我猜有人痴迷于auto 认为这更容易阅读。
  • @LightnessRacesinOrbit 模板参数推导。由于 lambda,您实际上无法编写 Final_action&lt;???&gt; 的类型。
  • 再次阅读本章。复制构造函数和operator= 都设置为删除以防止Final_action 被复制。此外,您的示例代码在cout 之后至少缺少一个分号
  • @btshengsheng 一个 lambda 表达式创建一个函数对象(函子),它的类型是由编译器生成的。程序员不知道该类型的名称,但除此之外它是一个带有operator () 的常规类。

标签: c++ raii


【解决方案1】:

您是对的,此代码已损坏。它仅在应用return value optimizations 时才能正常工作。这一行:

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

可能会或可能不会调用复制构造函数。如果是这样,那么您将拥有两个 Final_action 类型的对象,因此您将调用该 lambda 两次。

【讨论】:

  • 可能有人说“嗯,这就是它在 my 系统上的工作方式”,并没有足够关心编写远程可移植代码.. 甚至是正确失败的代码:(
  • @Quentin:更简单的修复:auto&amp;&amp; -- 延长临时的生命周期以匹配 act1,防止任何复制
  • @BenVoigt 这会起作用,但会在现场留下地雷。
  • 章节好像更新了:Here vs Here
  • @SimonKraemer 更新的代码也不正确,请参阅 Arne Vogel 的回答。
【解决方案2】:

最简单的解决办法是

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";} );

使用复制列表初始化和延长生命周期的转发引用避免了Final_action 对象的任何复制/移动。 Copy-list-initialization 直接构造临时的Final_action 返回值,finally 返回的临时值通过绑定到act1 来延长其生命周期——同样无需任何复制或移动。

【讨论】:

  • 工作正常!对于那些可能想知道的人,返回语句中的大括号return { std::move(f) } 调用了复制列表初始化,请参阅here
【解决方案3】:

代码已损坏。 SimonKraemer 提到的修改后的代码被破坏了——它不能编译(finally 中的 return 语句是非法的,因为Final_action 既不可复制也不可移动)。使用生成的移动构造函数使Final_action 仅移动也不起作用,因为F 保证具有移动构造函数(如果没有,则Final_action 生成的移动构造函数将静默使用F' s 复制构造函数作为后备),也不能保证 F 在移动后是空操作。事实上,示例中的 lambda not 会变成无操作。

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

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

或者,删除Final_action 的模板参数并将其更改为使用std::function&lt;void()&gt;。在调用它之前检查clean 是否为空。添加将原始std::function 设置为nullptr 的move c'tor 和move assignment。 (是的,这是可移植的。移动std::function保证源将为空。) 优点:类型擦除的通常好处,例如能够返回范围保护到外部堆栈框架而不暴露F。缺点:可能会增加大量运行时间开销。

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

更新:Stroustrup 的新示例甚至无法编译。我错过了明确删除副本 c'tor 也会禁用移动 c'tor 的生成。

【讨论】:

  • SimonKraemer 修改的代码确实不会编译(我用g++测试过),因为正如你所说,复制时不会自动生成移动构造函数构造函数被显式声明(被删除),参见here
  • 在阅读了您的第一段 6 次之后,我想我终于完全明白了您的意思 :-)。我假设您在答案的第四行错过了 noither ?顺便说一句,看到 Stroustrup 一遍又一遍地生成不正确的代码非常有趣……谢谢。
猜你喜欢
  • 2015-12-14
  • 2015-05-02
  • 2014-11-29
  • 2010-10-14
  • 1970-01-01
  • 2010-10-16
  • 1970-01-01
  • 1970-01-01
  • 2017-08-06
相关资源
最近更新 更多