依赖于RVO实现finally函数

9

在阅读《C++程序设计语言》(第4版)的异常处理章节时,有一个ad hoc清理代码的示例辅助工具:

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

它被用作

auto act1 = finally([&]{ delete p; });

在声明act1的代码块结束时运行lambda代码。
我想当Stroustrup测试它时,由于“返回值优化”将Final_action<>限制为单个实例,这样做是有效的-但RVO只是一种可选的优化吗?如果从finally返回时实例被复制,显然~Final_action()将运行两次。换句话说,p被删除两次。
这种行为是否有标准中的防止措施,还是代码足够简单以便大多数编译器可以对其进行优化?

自C++17以来,在某些情况下RVO是保证的 - Jesper Juhl
@JesperJuhl 但是这本书是在C++17之前的。我认为这本书是错误的。如果你传递-fno-elide-constructors,结果肯定是错误的。 - llllllllll
保证的 RVO 仅适用于 纯右值。在任何时候,如果您的对象具有 名称,则某些赌注就会关闭。此外,在 C++17 中,prvalues 的含义略有不同。但还有另一个我不确定的复杂情况。 - WhiZTiM
我认为Bjarne的意图是演示概念,而不是呈现一个生产质量的作用域守卫。是的,这可以被打破,但展示修复它的完整代码是否有助于或削弱了书中该部分试图传达的观点?我不知道。 - StoryTeller - Unslander Monica
今天已经是第10次了,"c++永远不会在核心语言中内置'finally'语义,因为没有这样的需求"。 - pm100
2个回答

2

事实上,该示例依赖于复制省略,这只有在C++17中才能保证(在某些情况下)。

话虽如此,复制省略是大多数现代C++11/C++14编译器实现的优化。如果在优化构建中失败,我会感到惊讶。

但是,如果您想使其强壮无比,可以添加移动构造函数:

template<typename F>
struct Final_action {
    Final_action(F f): clean{f} {}
    ~Final_action() { if (!moved) clean(); }
    Final_action(Final_action&& o) : clean(std::move(o.clean)) {o.moved = true;}
private:
    F clean;
    bool moved{false};
};

template<class F>
Final_action<F> finally(F f)
{
    return Final_action<F>(f);
}

我认为这并不是必需的。事实上,大多数编译器即使您没有启用优化,也会执行复制省略。其中包括gccclangiccMSVC。这是因为复制省略在标准中是明确允许的。

如果您添加此片段:

int main() {
    int i=0;
    {
        auto f = finally([&]{++i;});
    }
    return i;
}

如果你在godbolt.org上分析生成的汇编输出,你会发现Final_action::~Final_action()通常只被调用一次(在main()中)。启用优化后,编译器甚至更加积极:仅启用-O1gcc 4.7.1的输出如下:

main:
  mov eax, 1 # since i is incremented only once, the return value is 1.
  ret

0

这仅适用于C++17及以上版本! 在C++11或C++14中,由于删除了复制构造函数,它会失败。自C++17以来,有强制执行RVO的情况,因此不需要复制/移动构造函数。


如果实例被复制了[...]。
那么,禁止进行复制怎么样?
template<typename F>
struct Final_action {
  Final_action(F f): clean{f} {}
  Final_action(Final_action const &) = delete;
  Final_action & operator=(Final_action const &) = delete;
  ~Final_action() { clean(); }
  F clean;
};

如果您已经在使用boost,那么可以从boost::noncopyable派生。

有关防止复制的进一步讨论。


#include <iostream>

template<typename F>
struct Final_action {
  Final_action(F f): clean{f} {}
  Final_action(Final_action const &) = delete;
  Final_action & operator=(Final_action const &) = delete;
  ~Final_action() { clean(); }
  F clean;
};

template<class F>
Final_action<F> finally(F f)
{
  return Final_action<F>(f);
}

int main() {
  auto _ = finally([]() { std::cout << "Bye" << std::endl; });
}

如果防止复制Final_action,则finally的实例化将无法编译 - 例如使用gcc:fin.cc:19:41: error: use of deleted function ‘Final_action<F>::Final_action(const Final_action<F>&) [with F = main()::<lambda()>]’ auto act1 = finally([&]{ delete p; }); ^ fin.cc:4:5: note: declared here Final_action(Final_action const &) = delete; 复制构造函数可能不会(也必须不会)被调用,但必须是可访问的。 - vbar
@vbar 确实,这似乎只在C++17之后可行。 - Daniel Jour

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接