1

I want to insert into several STL containers simultaneously, and either have all succeed, or all of them remain unchanged. How can I do this elegantly?

In my example, I have a custom container with various STL containers as data members. An insertion operation inserts into various containers, but if one of them fails, the changes need to be undone to ensure all invariants are still valid. How can I do this (nicely)?

My first intuition is to go backwards through all insertions that were made and undo them. This seems excessive though as I hope my example will illustrate.

Example (Sorry if not "minimal enough"):

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");
  }
}

Even without writing out the cleanups, this seems excessive. Especially considering that insert and push_back should rarely fail to begin with. Is this really what I need to do?

Also, the safest way to erase changes made that I could determine for the std::unordered_set's and the std::map was a combination of find and erase, but I couldn't find anything on find's exception safety. Does it always succeed?

I occurred to me that the insert and push_back statements would have to be wrapped in a try block to truly handle the exception, but what exception do I catch?

Jasper Braun
  • 109
  • 8
  • 2
    If your elements can be *moved* without throwing exceptions (strong exception guarantee) you could *move construct* your elements into the data structure using `emplace()`. Would that be an option? – Galik Jul 18 '20 at 16:34
  • 1
    Also the docs say that [std::unordered_set::insert](https://en.cppreference.com/w/cpp/container/unordered_set/insert) will have no effect if any operation throws an exception. Could you insert the relevant elements into local data structures and then, insert those local structures into the member data structures at the end? – Galik Jul 18 '20 at 16:37
  • @Galik The problem is not necessarily that `emplace` or `insert` throw an exception. If they fail to execute and the `MyContainer` data members remain unchanged, then the invariant may be violated. In that case, in order to make sure the invariant isn't violated, (an thus basic exception safety guarantee) I would have to clean up the changes made thus far. – Jasper Braun Jul 18 '20 at 16:59
  • @Galik I don't know how to create single object locally and then move it into the containers. Especially, for the `std::vector`, since it's a contiguous memory container, I don't think I can truly move a locally constructed or allocated object into it. I can always copy the entire containers insert into the copies and move into the data member, but that would be expensive. – Jasper Braun Jul 18 '20 at 17:01
  • 1
    What about performing all your inserts into a fresh, empty local `std::vector` and then at the end perform one gigantic `std::insert` from your local vector to the member? (maybe using https://stackoverflow.com/questions/10720122/is-there-a-standard-way-of-moving-a-range-into-a-vector). – Galik Jul 18 '20 at 17:03
  • @Galik ends up being the same problem. Docs say if `insert` fails, there are no changes to the vector. I think the problem is that there are multiple containers. So I would have to simultaneously attempt to insert to all of them and if any of them fail, then all of them remain unchanged. – Jasper Braun Jul 18 '20 at 17:07
  • 1
    But if you *move construct* them from the local containers to the members would you be able to achieve a strong exception guarantee like that? (memory allocation excepted). – Galik Jul 18 '20 at 17:10
  • I am not sure how to do this simultaneously to all containers. I have reworded my question slightly. Hopefully it's more clear now what I want. Thanks for helping me realize my own question. – Jasper Braun Jul 18 '20 at 17:13
  • 1
    Why can't you just use a failed flag instead of all these throw statements? The loops should only continued to be executed if failed is false. Then you have one clean up function that will only be executed if failed is true to undo all that. – stackoverblown Jul 18 '20 at 21:51
  • @stackoverblown I believe this approach may be what I was looking for. I am unsure what you mean by a failed flag. Is it the return value for some internal `InsertHelper` function, so that `Insert` calls `InsertHelper` and if return value indicates failure, `Insert` calls an additional cleanup function? So far this seems like the best answer, and I will write it out as the official answer if no better pops up. – Jasper Braun Jul 19 '20 at 12:01

2 Answers2

0

If you can modify the constructor of DataItem, you can make it insert stuff to the next container from the constructor of the previous item. Here's a working demo

  #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";
  }

The program prints four zeros as expected.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • This does address "simultaneous" insertion. While not included in the question, it would not be possible, however, if there is no accessibility to the code of the data items. I believe for that reason the answer @stackoverblown suggested in the comments would be a better answer. – Jasper Braun Jul 19 '20 at 12:04
  • @Since all the containers are yours, the data items you insert in them is also yours. You can inherit from/contain the original data items you want to insert, and add a constructor you need. – n. m. could be an AI Jul 19 '20 at 13:42
  • For minimality in my example, I wrote a random `DataItem` struct to illustrate a point. Thus, you answer correctly responds to the question. However, @stackoverblown's strategy works without modifying the code of `DataItem`. Therefore, I believe that answer to be the better choice. – Jasper Braun Jul 19 '20 at 14:45
0

I don't address the actual clean up operation nor what is the most efficient way to do that. You will need to record at what stage the insertion failed and proceed accordingly.

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();
}
stackoverblown
  • 734
  • 5
  • 10
  • In my application, I ended up not using `try` - `catch` inside `Insert`. Instead, I put `Insert` in a wrapper function which wraps `Insert` itself in a `try` - `catch` and calls `CleanUp` in the `catch` block. That way, there were fewer `try` - `catch` blocks. If one of the STL `insert`s or `push_back`s throw then `Insert` is exited and no further unnecessary attempts to `insert` or `push_back` are made. – Jasper Braun Jul 19 '20 at 14:50