Kai
Kai

Reputation: 2832

Exception safety in C++ when adding to multiple std containers

I have some code that adds to a std::vector and a std::map after creating an object.

v.push_back(object);     // std::vector
m[object->id] = object;  // std::map

I want to make this have a strong exception guarantee. Normally, to make operations like these atomic, I would implement a swap method for each container, and call all of the functions that could throw on temporary copies of the container:

vector temp_v(v);
map temp_map(m);

temp_v.push_back(object);
temp_m[object->id] = object;

// The swap operations are no-throw
swap(temp_v, v)
swap(temp_m, m)

However, making temporary copies of the entire vector and map seems very expensive. Is there any way to implement a strong exception guarantee for this function without the expensive copies?

Upvotes: 13

Views: 1027

Answers (4)

Georg Fritzsche
Georg Fritzsche

Reputation: 98974

You can use a scopeguard object that rolls back the operations when destroyed unless it is told not to. This approach is outlined in Generic: Change the Way You Write Exception-Safe Code — Forever.

E.g. something like:

container1.push_back(a);
Guard g(bind(&ContainerType::pop_back, &container1));
container2.push_back(a);
// ...
g.dismiss();

Upvotes: 4

Sjoerd
Sjoerd

Reputation: 6875

General Case

Technically, only one copy is required:

  1. Copy the vector
  2. Update the copy
  3. Update the map
  4. Swap the copy and the original vector

Another option is catch-roll-back-and-rethrow:

  v.push_back(object);
  try
  {
    m.insert(object->id, object); // Assuming it cannot be present yet
  }
  catch(..)
  {
    v.pop_back();
    throw;
  }

Or the other way around. I picked this order because the vector::pop_back() is guaranteed not to fail.

UPDATE: If object->id could be present, see Grizzly's answer for a solution.


Specific Case for Pointers

However, as you are using object->, you might be storing pointers. The copy-constructor of a pointer cannot throw, and we can use that fact to simplify the code:

v.reserve(v.size() + 1);
m[object->id] = object; // can throw, but not during the assignment
v.push_back(object); // will not throw: capacity is available, copy constructor does not throw

And if you are really worried about frequent resizing:

if (v.capacity() == v.size()) v.resize(v.size()*2); // or any other growth strategy
m[object->id] = object;
v.push_back(object);

Upvotes: 4

Grizzly
Grizzly

Reputation: 20191

I think this is a situation where using try-catch is the correct manner of handling it. If the access to the map throws you undo the operation on the vector and rethrow:

v.push_back(object);  
try 
{
  m[object->id] = object;
} 
catch(...)
{
  v.pop_back();
  throw;
}

However this will still not give you a strong guarantee, since operator[] on maps is a problematic instruction for exception safety (if the element is not in the map an object is default constructed, which will stay in the map, if the operator= throws (very unlikely in this case, since you seem to be storing pointers, but still).

Therefore I would rewrite it as

try 
{
  auto iter = m.find(object->id);//used auto for shorter code,
  if(iter == m.end())
    m.insert(std::make_pair(object->id, object);
 else
   iter->second = object;
}

Upvotes: 4

ruakh
ruakh

Reputation: 183221

I suppose you could roll your own RAII-type object:

template<typename T>
class reversible_vector_pusher
{
  private:
    std::vector<T> * const ptrV;
    bool committed = false;
  public:
    reversible_vector_pusher(std::vector<T> & v, const T & obj) : ptrV(&v)
        { v.push_back(obj); }
    void commit()
        { committed = true; }
    ~reversible_vector_pusher()
    {
        if(! committed)
             ptrV->pop_back();
    }
};



reversible_vector_pusher<...> rvp(v, object); // replace ... with object's type
m[object->id] = object;
rvp.commit();

(I chose to reverse the vector-push because it's always reversible, whereas with a map you might have overwritten another element that you'd have to try to get back.)

Upvotes: 2

Related Questions