如何设计具有强异常保证的函数?
How do I design a function with a strong exception guarantee?
我有一个函数,我希望它有强大的异常保证:
class X {
/* Fields and stuff */
void some_function() {
vector1.push_back(/*...*/); // May Throw
vector2.push_back(/*...*/); // May Throw
vector3.push_back(/*...*/); // May Throw
vector4.push_back(/*...*/); // May Throw
}
};
我能想到的使它具有强大异常保证的唯一方法如下:
class X {
/* Fields and stuff */
void some_function() {
try { vector1.push_back(/*...*/);};
catch (Vector1PushBackException) {
throw Vector1PushBackException;
}
try { vector2.push_back(/*...*/);};
catch (Vector2PushBackException) {
vector1.pop_back();
throw Vector2PushBackException;
}
try { vector3.push_back(/*...*/);};
catch (Vector3PushBackException) {
vector1.pop_back();
vector2.pop_back();
throw Vector3PushBackException;
}
try { vector4.push_back(/*...*/);};
catch (Vector4PushBackException) {
vector1.pop_back();
vector2.pop_back();
vector3.pop_back();
throw Vector4PushBackException;
}
}
};
但是,这样真的很难看而且容易出错!!有没有比我上面提到的更好的解决方案?我能听到有人告诉我我需要使用 RAII,但我无法弄清楚如何,因为 pop_back
操作不能在函数 returns 正常时进行。
我还希望任何解决方案都为零 - 快乐之路上的开销;我真的需要快乐之路越快越好
解决方法是使用scope guards.
有关它们的示例实现,请参阅 this answer;我不打算在这里重复。使用范围守卫,您的代码将如下所示:
vector1.push_back(/*...*/);
FINALLY_ON_THROW( vector1.pop_back(); )
vector2.push_back(/*...*/);
FINALLY_ON_THROW( vector2.pop_back(); )
vector3.push_back(/*...*/);
FINALLY_ON_THROW( vector3.pop_back(); )
vector4.push_back(/*...*/);
FINALLY_ON_THROW( vector4.pop_back(); )
这里,FINALLY_ON_THROW
是一个宏(见上面的link)。它不是立即执行它的参数,而是在您因异常离开当前范围时执行它。如果您改为以正常方式离开作用域,则该参数将被忽略。如果您在控制首先到达守卫之前离开范围,它也会被忽略。
请注意,如果最后一个守卫(在同一范围内)没有任何东西可以抛出,那么最后一个守卫是多余的。
这个怎么样?
class X {
/* Fields and stuff */
void some_function() {
vector1.push_back(/*...*/); // May Throw
try {
vector2.push_back(/*...*/); // May Throw
try {
vector3.push_back(/*...*/); // May Throw
try {
vector4.push_back(/*...*/); // May Throw
} catch(...) {
vector3.pop_back();
throw;
}
} catch(...) {
vector2.pop_back();
throw;
}
} catch(...) {
vector1.pop_back();
throw;
}
}
};
但是……你真的需要 4 个不同的载体吗?
class X {
/* Fields and stuff */
void some_function() {
vector1234.push_back(std::make_tuple(/*...*/, /*...*/, /*...*/, /*...*/)); // May Throw
}
};
简单弹回的一个问题是它不一定会将矢量恢复到原始状态。如果添加元素导致任何向量重新分配,则对元素的迭代器/引用将失效,并且该失效无法回滚,从而无法实现强异常保证。
一个安全、简单且通用的解决方案是在副本上进行修改。复制当然需要额外的费用。
void some_function() {
auto copy = *this;
copy.vector1.push_back(/*...*/); // May Throw
copy.vector2.push_back(/*...*/); // May Throw
copy.vector3.push_back(/*...*/); // May Throw
copy.vector4.push_back(/*...*/); // May Throw
*this = std::move(copy);
}
scope guard suggestion 在可能回滚的情况下是一个优雅的解决方案,例如如果您使用另一个不会使迭代器/引用无效的容器,或者如果您根本不关心无效或者如果你有一个 class 不变量来阻止函数在容量已满时被调用。
你可以可行地,并以边际额外成本,首先检查是否所有向量都有额外的容量,并根据检查在复制和回滚之间进行选择。如果他们事先预留了足够的容量,这允许调用者不支付复制的成本。然而,这确实偏离了优雅。
您可以通过多种方式做到这一点...例如像这样:
#include <vector>
#include <type_traits>
#include <exception>
template<class F>
struct on_fail
{
F f_;
int count_{ std::uncaught_exceptions() };
~on_fail()
{
// C++20 is here and still no easy way to tell "unwinding" and "leaving scope" apart
if (std::uncaught_exceptions() > count_) f_();
}
};
template<class F> on_fail(F) -> on_fail<F>;
auto emplace_back_x(auto& v, auto&& x)
{
v.emplace_back(std::forward<decltype(x)>(x));
return on_fail{[&v]{ v.pop_back(); }};
}
int bar();
template<class F>
struct inplacer
{
F f_;
operator std::invoke_result_t<F&>() { return f_(); }
};
template<class F> inplacer(F) -> inplacer<F>;
void foo()
{
std::vector<int> v1, v2, v3;
auto rollback1 = emplace_back_x(v1, 1);
auto rollback2 = emplace_back_x(v2, inplacer{ bar });
auto rollback3 = emplace_back_x(v3, inplacer{ []{ return bar() + 1; } });
}
请注意,您的示例不正确:如果 push_back()
因 std::bad_alloc
(或任何其他异常)而失败 - 您无法执行撤消步骤。
此外,也许在您的情况下使用基本保证有意义吗?在实践中,您通常可以在更高层次上处理它——例如断开连接并丢弃整个累积状态,让客户端重新连接并重复尝试。
我有一个函数,我希望它有强大的异常保证:
class X {
/* Fields and stuff */
void some_function() {
vector1.push_back(/*...*/); // May Throw
vector2.push_back(/*...*/); // May Throw
vector3.push_back(/*...*/); // May Throw
vector4.push_back(/*...*/); // May Throw
}
};
我能想到的使它具有强大异常保证的唯一方法如下:
class X {
/* Fields and stuff */
void some_function() {
try { vector1.push_back(/*...*/);};
catch (Vector1PushBackException) {
throw Vector1PushBackException;
}
try { vector2.push_back(/*...*/);};
catch (Vector2PushBackException) {
vector1.pop_back();
throw Vector2PushBackException;
}
try { vector3.push_back(/*...*/);};
catch (Vector3PushBackException) {
vector1.pop_back();
vector2.pop_back();
throw Vector3PushBackException;
}
try { vector4.push_back(/*...*/);};
catch (Vector4PushBackException) {
vector1.pop_back();
vector2.pop_back();
vector3.pop_back();
throw Vector4PushBackException;
}
}
};
但是,这样真的很难看而且容易出错!!有没有比我上面提到的更好的解决方案?我能听到有人告诉我我需要使用 RAII,但我无法弄清楚如何,因为 pop_back
操作不能在函数 returns 正常时进行。
我还希望任何解决方案都为零 - 快乐之路上的开销;我真的需要快乐之路越快越好
解决方法是使用scope guards.
有关它们的示例实现,请参阅 this answer;我不打算在这里重复。使用范围守卫,您的代码将如下所示:
vector1.push_back(/*...*/);
FINALLY_ON_THROW( vector1.pop_back(); )
vector2.push_back(/*...*/);
FINALLY_ON_THROW( vector2.pop_back(); )
vector3.push_back(/*...*/);
FINALLY_ON_THROW( vector3.pop_back(); )
vector4.push_back(/*...*/);
FINALLY_ON_THROW( vector4.pop_back(); )
这里,FINALLY_ON_THROW
是一个宏(见上面的link)。它不是立即执行它的参数,而是在您因异常离开当前范围时执行它。如果您改为以正常方式离开作用域,则该参数将被忽略。如果您在控制首先到达守卫之前离开范围,它也会被忽略。
请注意,如果最后一个守卫(在同一范围内)没有任何东西可以抛出,那么最后一个守卫是多余的。
这个怎么样?
class X {
/* Fields and stuff */
void some_function() {
vector1.push_back(/*...*/); // May Throw
try {
vector2.push_back(/*...*/); // May Throw
try {
vector3.push_back(/*...*/); // May Throw
try {
vector4.push_back(/*...*/); // May Throw
} catch(...) {
vector3.pop_back();
throw;
}
} catch(...) {
vector2.pop_back();
throw;
}
} catch(...) {
vector1.pop_back();
throw;
}
}
};
但是……你真的需要 4 个不同的载体吗?
class X {
/* Fields and stuff */
void some_function() {
vector1234.push_back(std::make_tuple(/*...*/, /*...*/, /*...*/, /*...*/)); // May Throw
}
};
简单弹回的一个问题是它不一定会将矢量恢复到原始状态。如果添加元素导致任何向量重新分配,则对元素的迭代器/引用将失效,并且该失效无法回滚,从而无法实现强异常保证。
一个安全、简单且通用的解决方案是在副本上进行修改。复制当然需要额外的费用。
void some_function() {
auto copy = *this;
copy.vector1.push_back(/*...*/); // May Throw
copy.vector2.push_back(/*...*/); // May Throw
copy.vector3.push_back(/*...*/); // May Throw
copy.vector4.push_back(/*...*/); // May Throw
*this = std::move(copy);
}
你可以可行地,并以边际额外成本,首先检查是否所有向量都有额外的容量,并根据检查在复制和回滚之间进行选择。如果他们事先预留了足够的容量,这允许调用者不支付复制的成本。然而,这确实偏离了优雅。
您可以通过多种方式做到这一点...例如像这样:
#include <vector>
#include <type_traits>
#include <exception>
template<class F>
struct on_fail
{
F f_;
int count_{ std::uncaught_exceptions() };
~on_fail()
{
// C++20 is here and still no easy way to tell "unwinding" and "leaving scope" apart
if (std::uncaught_exceptions() > count_) f_();
}
};
template<class F> on_fail(F) -> on_fail<F>;
auto emplace_back_x(auto& v, auto&& x)
{
v.emplace_back(std::forward<decltype(x)>(x));
return on_fail{[&v]{ v.pop_back(); }};
}
int bar();
template<class F>
struct inplacer
{
F f_;
operator std::invoke_result_t<F&>() { return f_(); }
};
template<class F> inplacer(F) -> inplacer<F>;
void foo()
{
std::vector<int> v1, v2, v3;
auto rollback1 = emplace_back_x(v1, 1);
auto rollback2 = emplace_back_x(v2, inplacer{ bar });
auto rollback3 = emplace_back_x(v3, inplacer{ []{ return bar() + 1; } });
}
请注意,您的示例不正确:如果 push_back()
因 std::bad_alloc
(或任何其他异常)而失败 - 您无法执行撤消步骤。
此外,也许在您的情况下使用基本保证有意义吗?在实践中,您通常可以在更高层次上处理它——例如断开连接并丢弃整个累积状态,让客户端重新连接并重复尝试。