具有强大异常保证的同时 STL 容器插入

Simultaneous STL container insertion with strong exception guarantee

我想同时插入多个STL容器,要么全部成功,要么全部不变。我怎样才能优雅地做到这一点?

在我的示例中,我有一个自定义容器,其中包含各种 STL 容器作为数据成员。插入操作插入到各种容器中,但如果其中一个容器失败,则需要撤消更改以确保所有不变量仍然有效。我怎样才能(很好地)做到这一点?

我的第一直觉是回溯所有已插入的内容并撤消它们。这似乎过分了,但我希望我的例子能说明这一点。

示例(抱歉,如果不是“足够小”):

struct DataItem {
  int x;
  float y;
  char c;
};

class MyContainer {
public:
  void Insert(std::vector<std::string> names,
              std::unordered_set<int> categories,
              DataItem data);
private:
  /* invariant: if a string 'name' is mapped to an id 'i' in 'name_to_id_'
                it must be the position of a 'DataItem' object in 'data_'
                and contained in at least one of the categories */
  std::map<std::string, int> name_to_id_;
  std::map<int, std::unordered_set<int>> categories_;
  std::vector<DataItem> data_;
};

MyContainer::Insert(std::vector<std::string> names,
                    std::unordered_set<Categories> categories,
                    DataItem data) {
  /* ensure names, and categories are not empty and data is valid */
  int id = data_.size();
  for (auto it = names.begin(); it != names.end(); ++it) {
    if (this->name_to_id_.count(name)) {
      /* Clean up all names inserted so far by iterating backwards to names.begin()
         using this->name_to_id_.erase */
      throw SomeException("Insertion failed");
    }
    try {
      this->name_to_id_.insert({*it, id});
    } catch (/* what do I catch here? */) {
      /* Clean up all names inserted so far by iterating backwards to names.begin()
         using this->name_to_id_.erase */
      throw SomeException("Insertion failed");
    }
  }
  for (auto it = categories.begin(); it != categories.end(); ++it) {
    try {
      this->categories_.at(*it).insert(id);
    } catch (/* what do I catch here? */) {
      /* Clean up all names and all categories id was inserted into so far by
         iterating backwards to categories.begin() using
         this->categories_.at(*it).erase(id) */
      throw SomeException("Insertion failed");
    }
  }
  try {
    this->data_.push_back(data);
  } catch (/* what do I catch here? */) {
    /* Once again clean up all categories and names... */
    throw SomeException("Insertion failed");
  }
}

即使不写出清理,这也似乎过分了。特别是考虑到 insertpush_back 应该很少失败。这真的是我需要做的吗?

此外,我可以确定对 std::unordered_setstd::map 所做的更改的最安全方法是 finderase 的组合,但我找不到任何关于 find 的异常安全性的信息。它总是成功吗?

我想到 insertpush_back 语句必须包含在 try 块中才能真正处理异常,但我捕获了什么异常?

如果你可以修改DataItem的构造函数,你可以让它从上一个项目的构造函数中插入东西到下一个容器中。这是一个工作演示

  #include <vector>
  #include <list>
  #include <functional>
  #include <iostream>
  
  struct mydata
  {   
      int a;
      double b;
      template <typename F>                   
      mydata (int a, double b, F func) : a(a), b(b) { func(); }
  };                                                           
    
  int main()
  {         
      std::vector<mydata> one;
      std::vector<mydata> two;
      std::list<mydata> three;
      std::list<mydata> four; 
                             
      try {               
          one.emplace_back  (1, 2, [&]() {
          two.emplace_back  (1, 2, [&]() { 
          three.emplace_back(1, 2, [&]() { 
          four.emplace_back (1, 2, [ ]() { 
          throw 42; // simulate failure at the last one
          }); }); }); });
      } catch (int x)  {                                          
          // handle the failure
      }                 
       
      std::cout << one.size() << " " 
                << two.size() << " " 
                << three.size() << " " 
                << four.size() << "\n";
  }

程序按预期打印四个零。

我没有说明实际的清理操作,也没有说明最有效的方法是什么。您需要记录插入在哪个阶段失败并进行相应处理。

struct DataItem {
   int x;
   float y;
   char c;
};

class SomeException : std::exception
{
public:
   SomeException(char *c) : std::exception(c) {};

};

class MyContainer {
public:
   void Insert(std::vector<std::string> names,
      std::unordered_set<int> categories,
      DataItem data);

private:
   void CleanUp();

private:
   /* invariant: if a string 'name' is mapped to an id 'i' in 'name_to_id_'
   it must be the position of a 'DataItem' object in 'data_'
   and contained in at least one of the categories */
   std::map<std::string, int> name_to_id_;
   std::map<int, std::unordered_set<int>> categories_;
   std::vector<DataItem> data_;
};


void MyContainer::CleanUp()
{
   // Do your clean up here
}

void MyContainer::Insert(std::vector<std::string> names,
   std::unordered_set<int> categories,
   DataItem data) 
{
   bool failed = false;

   /* ensure names, and categories are not empty and data is valid */
   int id = data_.size();
   for (auto it = names.begin(); it != names.end() && !failed; ++it) {
      if (this->name_to_id_.count(*it))
         failed = true;
      try {
         this->name_to_id_.insert({ *it, id });
      }
      catch (...) {
         failed = true;
      }
   }

   for (auto it = categories.begin(); it != categories.end() && !failed; ++it) {
      try {
         this->categories_.at(*it).insert(id);
      }
      catch (...) {
         failed = true;
      }
   }

   try {
      if (!failed)
         this->data_.push_back(data);
   }
   catch (...) {
      failed = true;
   }

   if (failed)
      CleanUp();
}