异常安全 - 可靠地回滚对象状态的模式

时间:2013-10-30 13:04:25

标签: c++ exception

我一直在考虑如何实现各种异常安全保证,特别是保证,即当异常发生时数据回滚到它的原始状态。

考虑以下,精心设计的例子(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();
}

这可以完成工作,但也不能很好地扩展。知道所有类型的内部结构,需要为每个不同的用户定义类型设置一个哨兵。

我现在的问题是:在尝试实施强烈的异常安全代码时会想到哪些其他模式,习语或首选行动方案?

3 个答案:

答案 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)MSVCGCCClag编译器上运行了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; }; 。)