Reputation: 2832
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
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
Reputation: 6875
Technically, only one copy is required:
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.
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
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
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