Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

exception

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?

like image 639
Kai Avatar asked Jan 13 '12 21:01

Kai


2 Answers

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);
like image 58
Sjoerd Avatar answered Nov 08 '22 13:11

Sjoerd


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;
}
like image 25
Grizzly Avatar answered Nov 08 '22 14:11

Grizzly