异常安全 - 用于可靠回滚对象状态的模式
Exception safety - patterns for reliably rolling back object state
我一直在思考如何实现各种异常安全保证,尤其是强保证,即发生异常时数据回滚到原始状态。
考虑以下精心设计的示例(C++11 代码)。假设有一个简单的数据结构存储一些值
struct Data
{
int value = 321;
};
并且某些函数modify()
对该值进行操作
void modify(Data& data, int newValue, bool throwExc = false)
{
data.value = newValue;
if(throwExc)
{
// some exception occurs, sentry will roll-back stuff
throw std::exception();
}
}
(可以看出这是多么做作)。假设我们想为modify()
提供强大的异常安全保证。在异常的情况下,Data::value
的值显然不会回滚到其原始值。人们可以天真地继续try
整个功能,在适当的catch
块中手动设置内容,这是非常乏味的,并且根本不可扩展。
另一种方法是使用一些作用域RAII
帮助程序 - 有点像哨兵,它知道在发生错误时要临时保存和恢复什么:
struct FakeSentry
{
FakeSentry(Data& data) : data_(data), value_(data_.value)
{
}
~FakeSentry()
{
if(!accepted_)
{
// roll-back if accept() wasn't called
data_.value = value_;
}
}
void accept()
{
accepted_ = true;
}
Data& data_ ;
int value_;
bool accepted_ = false;
};
该应用程序很简单,只需要在modify()
成功的情况下调用accept()
:
void modify(Data& data, int newValue, bool throwExc = false)
{
FakeSentry sentry(data);
data.value = newValue;
if(throwExc)
{
// some exception occurs, sentry will roll-back stuff
throw std::exception();
}
// prevent rollback
sentry.accept();
}
这样可以完成工作,但也不能很好地扩展。每个不同的用户定义类型都需要有一个哨兵,知道该类型的所有内部结构。
我现在的问题是:在尝试实现强异常安全代码时,会想到哪些其他模式、习语或首选行动方案?
一般来说,它被称为ScopeGuard习语。并不总是可以使用临时变量和交换来提交(尽管在可以接受时很容易) - 有时您需要修改现有结构。
Andrei Alexandrescu和Petru Marginean在下面的论文中详细讨论了它:"通用:改变你编写异常安全代码的方式 - 永远"。
有Boost.ScopeExit库,它允许在不编码辅助类的情况下制作保护代码。文档中的示例:
void world::add_person(person const& a_person) {
bool commit = false;
persons_.push_back(a_person); // (1) direct action
// Following block is executed when the enclosing scope exits.
BOOST_SCOPE_EXIT(&commit, &persons_) {
if(!commit) persons_.pop_back(); // (2) rollback action
} BOOST_SCOPE_EXIT_END
// ... // (3) other operations
commit = true; // (4) disable rollback actions
}
D
编程语言在语言中具有特殊的结构,用于此目的 - scope(failure)
Transaction abc()
{
Foo f;
Bar b;
f = dofoo();
scope(failure) dofoo_undo(f);
b = dobar();
return Transaction(f, b);
}:
Andrei Alexandrescu在他的演讲中展示了这种语言结构的优势:"D的三个不太可能的成功特征"
我已经对scope(failure)
适用于MSVC
、GCC
、Clag
和Intel
编译器的功能进行了依赖于平台的实现。它在图书馆:stack_unwinding。在C++11中,它允许实现非常接近D
语言的语法。这是在线演示:
int main()
{
using namespace std;
{
cout << "success case:" << endl;
scope(exit)
{
cout << "exit" << endl;
};
scope(success)
{
cout << "success" << endl;
};
scope(failure)
{
cout << "failure" << endl;
};
}
cout << string(16,'_') << endl;
try
{
cout << "failure case:" << endl;
scope(exit)
{
cout << "exit" << endl;
};
scope(success)
{
cout << "success" << endl;
};
scope(failure)
{
cout << "failure" << endl;
};
throw 1;
}
catch(int){}
}
输出为:
success case:
success
exit
________________
failure case:
failure
exit
通常的方法不是在出现异常时回滚,而是在没有异常的情况下提交。这意味着,首先以一种不一定会改变程序状态的方式做关键的事情,然后通过一系列非抛出操作来提交。
然后,您的示例将如下所示:
void modify(Data& data, int newValue, bool throwExc = false)
{
//first try the critical part
if(throwExc)
{
// some exception occurs, sentry will roll-back stuff
throw std::exception();
}
//then non-throwing commit
data.value = newValue;
}
当然,RAII在异常安全方面发挥着重要作用,但它并不是唯一的解决方案。
"try-and-commit"的另一个例子是copy-swap-idiomit:
X& operator=(X const& other) {
X tmp(other); //copy-construct, might throw
tmp.swap(*this); //swap is a no-throw operation
}
如您所见,这有时会以额外的操作为代价(例如,如果 C 的复制 ctor 分配内存),但这是您必须为异常安全付出的代价。
我在最后面对案件时发现了这个问题。
如果您想在不使用复制和交换的情况下确保提交或回滚语义,我建议您为所有对象提供代理并一致地使用代理。
这个想法是隐藏实现细节,并将对数据的操作限制为可以有效回滚的子集。
因此,使用数据结构的代码将是这样的:
void modify(Data&data) {
CoRProxy proxy(data);
// Only modify data through proxy - DO NOT USE data
... foo(proxy);
...
proxy.commit(); // If we don't reach this point data will be rolled back
}
struct Data {
int value;
MyBigDataStructure value2; // Expensive to copy
};
struct CoRProxy {
int& value;
const MyBigDataStructure& value2; // Read-only access
void commit() {m_commit=true;}
CoRProxy(data&d):value(d.value),value2(d.value2),
m_commit(false),m_origValue(d.value){;}
~CoRProxy() {if (!m_commit) std::swap(m_origValue,value);}
private:
bool m_commit;
int m_origValue;
};
重点是代理将接口限制为data
proxy
可以回滚的操作,并且(可选)提供对其余data
的只读访问权限。如果我们真的想确保没有直接访问data
我们可以将proxy
发送到新函数(或使用 lambda)。
一个类似的用例是使用向量并在发生故障时回滚push_back。
template <class T> struct CoRVectorPushBack {
void push_back(const T&t) {m_value.push_back(t);}
void commit() {m_commit=true;}
CoRVectorPushBack(std::vector<T>&data):
m_value(data),m_origSize(data.size()),m_commit(false){;}
~CoRVectorPushBack() {if (!m_commit) value.resize(m_origSize);}
private:
std::vector<T>&m_value;
size_t m_origSize;
bool m_commit;
};
这样做的缺点是需要为每个操作创建一个单独的类。好处是使用代理的代码简单明了且安全(我们甚至可以在push_back中添加if (m_commit) throw std::logic_error();
)。
- Constexpr替代了新的放置方式,可以让内存中的对象保持未初始化状态
- 在容器上移动分配:以前包含的对象的状态
- MATLAB:跟踪imufilter对象中的状态变化
- 是否很好地使用状态模式来维护当前选定的对象?
- "Extern"对象问题:错误:Id 返回 1 个退出状态
- 访问处于"移自"状态的对象
- 是 *this = Ctor();清除对象状态的合法和高效?
- 设计模式中对象中的过程(方法和操作)的状态
- 如何保持异步函数中使用成员的shared_ptr对象的活动状态?
- 无法访问/存储地图中的游戏状态对象
- 单元测试 - 设置私人成员以获得所需的对象状态
- 在不同的运行中保持对象状态
- 调用 ~Derived() 和 ~Base() 之间的对象状态
- 异常安全 - 用于可靠回滚对象状态的模式
- 从函数中更改对象状态
- 正在更改const方法中的对象状态
- c++中对象状态的重构
- C++RAII来管理对象状态的更改和恢复
- 从默认构造函数调用重载构造函数时,维护对象状态信息
- c++11中的线程安全对象状态操作