我一直在考虑如何实现各种异常安全保证,特别是强保证,即当异常发生时数据回滚到它的原始状态。
考虑以下,精心设计的例子(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;
};
该应用程序很简单,只需accept()
成功就可以调用modify()
:
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();
}
这可以完成工作,但也不能很好地扩展。知道所有类型的内部结构,需要为每个不同的用户定义类型设置一个哨兵。
我现在的问题是:在尝试实施强烈的异常安全代码时会想到哪些其他模式,习语或首选行动方案?
答案 0 :(得分:3)
一般来说,它被称为ScopeGuard idiom。并不总是可以使用临时变量和交换提交(尽管在可接受时很容易) - 有时您需要修改现有结构。
Andrei Alexandrescu和Petru Marginean在以下论文中详细讨论了这些问题:"Generic: Change the Way You Write Exception-Safe Code — Forever"。
有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在他的演讲中展示了该语言结构的优点:"Three Unlikely Successful Features of D"
我已经在scope(failure)
,MSVC
,GCC
和Clag
编译器上运行了Intel
功能的平台相关实现。它在库中:stack_unwinding。在C ++ 11中,它允许实现非常接近D
语言的语法。这是Online DEMO:
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
答案 1 :(得分:3)
通常的方法是在异常的情况下不回滚,而是在没有异常的情况下提交。这意味着,首先以不一定改变程序状态的方式执行关键任务,然后使用一系列非抛出操作进行提交。
您的示例将如下所示:
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-idiom:
X& operator=(X const& other) {
X tmp(other); //copy-construct, might throw
tmp.swap(*this); //swap is a no-throw operation
}
正如您所看到的,这有时会以额外的操作为代价(例如,如果C的复制ctor分配内存),但这是您需要为异常安全支付一些时间的代价。
答案 2 :(得分:0)
我在最后面对案件时发现了这个问题。
如果您想在不使用复制和交换的情况下确保提交或回滚语义,我建议为所有对象提供代理,并且始终使用代理。
这个想法是隐藏实现细节并将数据操作限制为可以有效回滚的子集。
所以使用数据结构的代码就是这样的:
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
可以回滚的操作,并且(可选)提供对{{1}的其余部分的只读访问权限}。如果我们确实希望确保无法直接访问data
,我们可以将data
发送到新函数(或使用lambda)。
类似的用例是使用向量并在发生故障时回滚push_back。
proxy
这样做的缺点是需要为每个操作创建一个单独的类。
好处是代码使用代理非常简单且安全(我们甚至可以在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;
};
。)