2

我一直在思考如何实现各种异常安全保障,尤其是保障,即异常发生时数据回滚到原来的状态。

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

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

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

4

3 回答 3

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 分配内存),但这是您必须为异常安全付出一些时间的代价。

于 2013-10-30T13:13:59.223 回答
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 在他的演讲中展示了这种语言结构的优势:“D 的三个不太可能成功的特征”


我已经对适用于 、 和 编译器的功能进行了依赖于平台的scope(failure)实现。它在库中:stack_unwinding。在 C++11 中,它允许实现非常接近语言的语法。这是在线演示MSVCGCCClagIntelD

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
于 2013-10-30T13:11:03.113 回答
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的操作,并且(可选)提供对. 如果我们真的想确保没有直接访问,我们可以将 发送到一个新函数(或使用 lambda)。proxydatadataproxy

一个类似的用例是使用向量并在失败的情况下回滚 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;
};

这样做的缺点是需要为每个操作创建一个单独的类。好处是使用代理的代码简单且安全(我们甚至可以添加if (m_commit) throw std::logic_error();push_back)。

于 2018-01-12T11:17:15.240 回答