How do I design a function with a strong exception guarantee?

252 Views Asked by At

I have a function which I would like to have the strong exception guarantee:

class X {
   /* Fields and stuff */
   void some_function() {
       vector1.push_back(/*...*/); // May Throw
       vector2.push_back(/*...*/); // May Throw
       vector3.push_back(/*...*/); // May Throw
       vector4.push_back(/*...*/); // May Throw
   }
};

The only way I can think of making this having the strong exception guarantee is the following:

class X {
   /* Fields and stuff */
   void some_function() {
       try { vector1.push_back(/*...*/);};
       catch (Vector1PushBackException) {
            throw Vector1PushBackException;
       }
       try { vector2.push_back(/*...*/);};
       catch (Vector2PushBackException) {
            vector1.pop_back();
            throw Vector2PushBackException;
       }
       try { vector3.push_back(/*...*/);};
       catch (Vector3PushBackException) {
            vector1.pop_back();
            vector2.pop_back();
            throw Vector3PushBackException;
       }
       try { vector4.push_back(/*...*/);};
       catch (Vector4PushBackException) {
            vector1.pop_back();
            vector2.pop_back();
            vector3.pop_back();
            throw Vector4PushBackException;
       }
   }
};


However, this is really ugly and error-prone!! Is there a better solution than the one I put above? I can hear someone telling me that I need to use RAII, but I can't figure it out how, since the pop_back operations must not be done when the function returns normally.

I would also like any solution to be zero - overhead on the happy path; I really need the happy path to be as fast as possible.

4

There are 4 best solutions below

3
HolyBlackCat On BEST ANSWER

The solution is to use scope guards.

See this answer for an example implementation of them; I'm not going to repeat it here. With scope guards, your code will look like this:

vector1.push_back(/*...*/);
FINALLY_ON_THROW( vector1.pop_back(); )
vector2.push_back(/*...*/);
FINALLY_ON_THROW( vector2.pop_back(); )
vector3.push_back(/*...*/);
FINALLY_ON_THROW( vector3.pop_back(); )
vector4.push_back(/*...*/);
FINALLY_ON_THROW( vector4.pop_back(); )

Here, FINALLY_ON_THROW is a macro (see link above). Rather than executing it's parameter immediately, it causes it to be executed when you leave the current scope because of an exception. If you instead leave the scope the normal way, the parameter is ignored. If you leave the scope before control reaches the guard in the first place, it's also ignored.

Note that the last guard is superfluous if nothing after it (in the same scope) can throw.

1
user253751 On

How about this?

class X {
   /* Fields and stuff */
   void some_function() {
       vector1.push_back(/*...*/); // May Throw
       try {
           vector2.push_back(/*...*/); // May Throw
           try {
               vector3.push_back(/*...*/); // May Throw
               try {
                   vector4.push_back(/*...*/); // May Throw
               } catch(...) {
                   vector3.pop_back();
                   throw;
               }
           } catch(...) {
               vector2.pop_back();
               throw;
           }
       } catch(...) {
           vector1.pop_back();
           throw;
       }
   }
};

But... do you really need 4 different vectors?

class X {
   /* Fields and stuff */
   void some_function() {
       vector1234.push_back(std::make_tuple(/*...*/, /*...*/, /*...*/, /*...*/)); // May Throw
   }
};
4
eerorika On

A problem with simply popping back is that it won't necessarily restore the vector to the original state. If adding the element caused any of the vectors to reallocate, then iterators / references to the elements would be invalidated and that invalidation cannot be rolled back making strong exception guarantee impossible.

A safe and simple and general solution is to make the modifications on a copy. Copying of course has an extra cost though.

void some_function() {
    auto copy = *this;
    copy.vector1.push_back(/*...*/); // May Throw
    copy.vector2.push_back(/*...*/); // May Throw
    copy.vector3.push_back(/*...*/); // May Throw
    copy.vector4.push_back(/*...*/); // May Throw
    *this = std::move(copy);
}

HolyBlackCat's scope guard suggestion is an elegant solution in cases where rollback is possible such as if you used another container that doesn't invalidate iterators / references, or if you simply don't care about the invalidation or if you have a class invariant that prevents the function from being called when the capacity is full.

You could feasibly, and at marginal extra cost, first check whether all vectors have extra capacity and choose between copying and rollback based on the check. This allows the caller to not pay the cost of copying if they reserve sufficient capacity beforehand. That does stray further from elegance however.

0
C.M. On

You can do it in many ways... For example like this:

#include <vector>
#include <type_traits>
#include <exception>


template<class F>
struct on_fail
{
    F   f_;
    int count_{ std::uncaught_exceptions() };

    ~on_fail()
    {
        // C++20 is here and still no easy way to tell "unwinding" and "leaving scope" apart
        if (std::uncaught_exceptions() > count_) f_();
    }
};

template<class F> on_fail(F) -> on_fail<F>;


auto emplace_back_x(auto& v, auto&& x)
{
    v.emplace_back(std::forward<decltype(x)>(x));
    return on_fail{[&v]{ v.pop_back(); }};
}


int bar();


template<class F>
struct inplacer
{
    F f_;
    operator std::invoke_result_t<F&>() { return f_(); }
};

template<class F> inplacer(F) -> inplacer<F>;


void foo()
{
    std::vector<int> v1, v2, v3;
    auto rollback1 = emplace_back_x(v1, 1);
    auto rollback2 = emplace_back_x(v2, inplacer{ bar });
    auto rollback3 = emplace_back_x(v3, inplacer{ []{ return bar() + 1; } });
}

Note that your example is not correct: if push_back() fails with std::bad_alloc (or any other exception) -- you fail to perform undo step.

Also, perhaps in your case it make sense to use basic guarantee? In practice you can often deal with it on a higher level -- e.g. disconnect and discard entire accumulated state, leaving client to reconnect and repeat attempt.