使用C++ lambda正确实现finally块

28

我想在我的C++程序中实现一个finally块,该语言肯定有相关的工具来实现,即使没有原生设施。我想知道最佳的方法是什么?


一直在等这个..谢谢。 - Koushik Shetty
一些C++编译器,比如C++Builder,实际上将finally作为本地编译器扩展来实现。 - Remy Lebeau
一些之前的尝试:http://stackoverflow.com/questions/14897392/are-there-issues-with-this-scope-guard-implementation,https://dev59.com/PnA65IYBdhLWcg3wuRF8,https://dev59.com/ueo6XIcBkEYKwwoYQiO7。 - kennytm
请注意,可能有一种更轻量级的解决方案:http://codereview.stackexchange.com/questions/19730/scope-exit-something-wrong - user1095108
@user1095108 这个答案和 Boost.ScopeExit 完全一样吗?此外,它并不更轻量级;它在核心上几乎相同,但不是异常安全的,并且似乎有一些我无法解释的额外功能。它似乎是从 C++03 代码移植而来,但不再与 C++03 兼容。 - Potatoswatter
显示剩余3条评论
2个回答

38
这个简单的实现似乎是100%安全的。
template< typename t >
class sentry {
    t o;
public:
    sentry( t in_o ) : o( std::move( in_o ) ) {}

    sentry( sentry && ) = delete;
    sentry( sentry const & ) = delete;

    ~ sentry() noexcept {
        static_assert( noexcept( o() ),
            "Please check that the finally block cannot throw, "
            "and mark the lambda as noexcept." );
        o();
    }
};

template< typename t >
sentry< t > finally( t o ) { return { std::move( o ) }; }
noexcept的重要性在于,当函数由于异常而退出时,您不希望抛出另一个异常。(这将导致立即终止。)C++不会检查lambda是否真的不能抛出任何东西;您需要手动检查并将其标记为noexcept。(请参见下文。)
工厂函数是必需的,否则就没有办法获得依赖于lambda的类型。
复制和移动构造函数必须被删除,因为它们可能被用来隐式生成临时对象,这将实现另一个sentry,这将在销毁时过早地调用块。但默认赋值运算符保持不变,因为如果您已经有两个执行不同操作的sentry,则可以将它们分配给它们。(有些理论,但无论如何。)
如果构造函数是explicit,那将很好,但似乎这将排除返回值的原地初始化。由于该类不可移动,因此在调用方的作用域中存在的对象必须由return语句中的表达式直接初始化。
要使用,只需像这样定义一个guard:
auto && working_state_guard = finally( [&]() noexcept {
    reset_working_state();
} );

绑定到引用是必要的,因为在调用作用域中声明实际对象将需要从函数返回值中移动初始化该对象。

大约在4.7版本,g++ -Wall 会警告该守卫未使用。无论您是否针对此进行编码,您都可以在函数末尾添加一个惯用语,以增加一些安全性和文档说明:

static_cast< void >( working_state_guard );

这让读者从作用域的开始就知道了代码的执行过程,它可以作为复制粘贴代码时提醒人们检查两遍的提示。


使用方法。

int main() {
    auto && guard = finally( []() noexcept {
        try {
            std::cout << "Goodbye!\n";
        } catch ( ... ) {
            // Throwing an exception from here would be worse than *anything*.
        }
    } );

    std::cin.exceptions( std::ios::failbit );
    try {
        float age;
        std::cout << "How old are you?\n";
        std::cin >> age;
        std::cout << "You are " << age << " years (or whatever) old\n";
    } catch ( std::ios::failure & ) {
        std::cout << "Sorry, didn't understand that.\n";
        throw;
    }
    static_cast< void >( guard );
}

这会生成类似以下的输出。
$ ./sentry 
How old are you?
3
You are 3 years (or whatever) old.
Goodbye!
$ ./sentry 
How old are you?
four
Sorry, didn't understand that.
Goodbye!
terminate called after throwing an instance of 'std::ios_base::failure'
  what():  basic_ios::clear
Abort trap: 6

如何取消操作的执行?

看一下一些“以前的尝试”,我发现有一个事务性的commit()方法。我认为它不应该属于ScopeGuard/finally块实现的范畴。实现协议是包含functor的责任,所以正确的分工应该是在其中封装一个布尔标志,例如通过捕获一个bool本地标志,并在事务完成时翻转标志。

同样,通过重新分配functor本身来取消操作只是一种混乱的方法。通常更喜欢在现有协议内部增加一个额外的情况,而不是围绕旧协议发明一个新协议。


1
另外,您可能希望提供一个更完整的示例用法。也许是一个包含try/catch和嵌入了最终用法示例的主函数。 - Andrew Tomazos
@Potatoswatter:当然,将函数对象设置为可选项意味着修改析构函数以在未设置时执行无操作。而且复制和移动之间有区别:移动构造应从移动的对象中窃取函数对象,因此从移动对象中取消设置它。你是对的,因为旨在模拟finally子句,所以移动对象毫无意义。我只是强调这一点,因为你只在回答中证明了没有复制构造函数,而没有移动构造函数。 - Laurent LA RIZZA
@LaurentLARIZZA 我说“复制和移动构造函数必须被删除,因为复制会创建...”,但是我的意思是复制或移动。我会修复的。无论如何,具体细节并不重要;我们只想防止意外创建对象。 - Potatoswatter
@Potatoswatter:好的,没问题 :) - Laurent LA RIZZA
顺便说一句:在看到类似的 finally(lambda) 语法后,我感到困惑,然后找到了这个问答。auto _ = finally([f] { fclose(f); }) // 记得关闭文件 参考:https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#r12-immediately-give-the-result-of-an-explicit-resource-allocation-to-a-manager-object - kevinarpe
显示剩余4条评论

9
使用std::function的另一种解决方案。 无需工厂函数。不需要每次使用模板实例化(占用更少的空间?!)。 不需要std::move和&&等内容,也不需要auto;)
class finally
{
    std::function<void()> m_finalizer;
    finally() = delete;

public:
    finally( const finally& other ) = delete;
    finally( std::function<void()> finalizer )
     : m_finalizer(finalizer)
    {
    }
    ~finally()
    {
        std::cout << "invoking finalizer code" << std::endl;
    if( m_finalizer )
        m_finalizer();
    }
};

使用方法:

int main( int argc, char * argv[] )
{
    bool something = false;
    try
    {
    try
    {
            std::cout << "starting" << std::endl;
        finally final([&something]() { something = true; });
        std::cout << "throwing" << std::endl;
        throw std::runtime_error("boom");
    }
    catch(std::exception & ex )
    {
        std::cout << "inner catch" << std::endl;
        throw;
    }
    }
    catch( std::exception & ex )
    {
    std::cout << "outer catch" << std::endl;
        if( something )
    {
        std::cout << "works!" << std::endl;
        }
        else
    {
        std::cout << "NOT working!" << std::endl;
        }
    }
    std::cout << "exiting" << std::endl;
    return 0;
}

输出:

开始

抛出异常

调用终结器代码

内部捕获

外部捕获

成功!

退出


这会增加间接函数调用的运行时开销,以及将 lambda 包装在 std::function 中的模板实例化开销。虽然被类型抹除隐藏了,但模板仍然存在。此外,这里的复制构造函数会导致 lambda 被调用两次。你应该删除复制构造函数,并且如果需要的话(似乎不需要),可以提供一个移动构造函数。 - Potatoswatter
std::function 内部的实例化至少包括转换构造函数(对应于工厂函数)和一个隐藏的容器类,该类提供间接调用语义(对应于哨兵类,但更复杂)。这些发生在每个不同的 lambda 中一次,或者说是“每次使用”。无论如何,现在这是一个有效的解决方案,所以 +1 :) - Potatoswatter
仍然让我担心的是 try、catch 和 finally 代码块的执行顺序。如果我没有弄错的话,在其他语言中,finally 段在 catch 代码执行之后才被执行,而在这里展示的两个解决方案中,finally 代码在 catch 代码段之前执行。 - BitTickler
2
在其他编程语言中,finally引入了一段代码块。在C++中,它是一个对象声明。该代码在对象作用域的末尾执行,因此如果您希望在catch之后发生,请在try之前声明它。 - Potatoswatter
构造函数应该是explicit的,并且它应该移动std::function而不是复制。如果您希望finally完全不可复制,那么赋值运算符也应该被删除。此外,默认构造函数已经被隐式删除了。 - Emile Cormier
显示剩余2条评论

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