【问题标题】:Correctly implement finally block using C++ lambda使用 C++ lambda 正确实现 finally 块
【发布时间】:2013-06-25 17:28:44
【问题描述】:

我想在我的 C++ 程序中实现一个 finally 块,如果不是本机工具,该语言当然有工具可以做到这一点。我想知道最好的方法是什么?

【问题讨论】:

标签: c++ c++11 finally


【解决方案1】:

这个简单的实现似乎是 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 的类型。

必须删除复制和移动构造函数,因为它们可用于隐式生成一个临时对象,该对象将实现另一个哨兵,该哨兵会在销毁时过早调用该块。但是默认分配运算符保持不变,因为如果您已经有两个执行不同操作的哨兵,则可以分配它们。 (有点理论,但无论如何。)

如果构造函数是explicit,那就太好了,但这似乎排除了返回值的就地初始化。由于该类不可移动,因此位于调用者范围内的对象必须直接由return 语句中的表达式初始化。

要使用,只需像这样定义一个守卫:

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 块实现。实现一个协议是包含的函子的责任,因此正确的分工应该是在其中封装一个布尔标志,例如捕获一个bool本地标志,并在事务完成时翻转该标志。

同样,试图通过重新分配函子本身来取消操作只是一种混乱的方法。通常更喜欢在现有协议中添加一个额外的案例,而不是围绕旧协议发明一个新协议。

【讨论】:

  • nitpick:字符串文字不能包含换行符。
  • 此外,您可能还想提供更完整的示例用法。可能是一个包含 try/catch 的 main 函数,并嵌入了示例 finally 用法。
  • @user1131467 谢谢,只是复制粘贴错误。也许在午餐后。
  • 我不明白你为什么禁用移动构造函数。如果您从一个类中删除复制语义,它很有可能仍然具有移动语义。 (您对删除复制语义的解释是理智的,但移动语义没有)我知道 o 成员在这种情况下必须是 std::optional 或 boost::optional ,以允许无资源状态。也许您应该为删除移动构造函数添加额外的理由。
  • @LaurentLARIZZA 复制和移动构造之间没有区别。问题是 either 会得到两个对象而无需调用 finally 两次。因为optional 不是函子,所以它不起作用。 (或者如果是,在为空时调用它肯定会引发异常,这会使它变得无用。)给定一个 optional_functor 具有无操作移出状态,你的逻辑成立,但我们没有这个和没有理由假设用户会提供这样的东西。
【解决方案2】:

使用 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 被调用两次。您应该删除复制构造函数,并在需要时提供一个移动构造函数(它似乎不是)。
  • 运行程序看看 - 它没有运行 lanbda 两次。析构函数调用 lambda,正如您在示例的打印输出中看到的那样,“~finally()”打印只显示一次。至于模板实例化 - 是的,有 1 个实例化,但不是每次使用 1 个。在另一种解决方案中,只要捕获表达式发生变化,模板就会被实例化。既适用于工厂,也适用于哨兵级。删除复制构造函数是不可能的,因为如果我尝试将函数作为引用传递,愚蠢的 c++11 会产生错误。我用clang在freebsd 10.1上运行了我的测试,顺便说一句。
  • 忘记我所说的复制构造函数……它是以前版本的遗留物。
  • std::function内部的实例化至少包括转换构造函数(对应于工厂函数)和一个提供间接调用语义的隐藏容器类(对应于哨兵类,但更复杂)。这些在每个不同的 lambda 或“每次使用”中出现一次。无论如何,现在这是一个有效的解决方案,所以 +1 :)
  • 在其他语言中,finally 引入了一段代码。在 C++ 中,它是一个对象声明。代码在对象作用域的末尾执行,所以如果你想让它发生在catch之后,只需在try之前声明它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-15
  • 2017-12-19
相关资源
最近更新 更多