这个简单的实现似乎是 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本地标志,并在事务完成时翻转该标志。
同样,试图通过重新分配函子本身来取消操作只是一种混乱的方法。通常更喜欢在现有协议中添加一个额外的案例,而不是围绕旧协议发明一个新协议。