这个简单的实现似乎是 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 ) }; }
The 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 );
这可以让读者从范围的一开始就了解代码的执行情况,并且可以提醒您在复制粘贴代码时进行仔细检查。
Usage.
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
本地标志,并在事务完成时翻转标志。
同样,尝试通过重新分配函子本身来取消操作也是一种混乱的方法。通常更喜欢在现有协议中添加一个额外的案例,而不是围绕旧协议发明一种新协议。