4

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.

SomeProgrammer
  • 1,134
  • 1
  • 6
  • 12
  • Strong exception guarantee is not always compatible with zero overhead. In this case, if you actually need strong exception guarantee, copy the original vector, modify the copy and `swap` once the risky parts are done. This is the most robust solution. – François Andrieux Apr 13 '21 at 19:21
  • When you have a `catch` block that is intended to rethrow, you can just use `throw;` to rethrow the exception. This is also the only way to rethrow from a universal `catch(...)` block. – François Andrieux Apr 13 '21 at 19:22
  • 1
    @Cory Kramer Yes that was a mistake, code edited. But the previous push_back might have succeeded (for example vector3 push_back failed but the previous 2 worked) so I still need to do some pop_back s. – SomeProgrammer Apr 13 '21 at 19:23
  • @FrançoisAndrieux I can't afford overhead on the happy path... I don't understand why c++ does not have a mechanism to provide a more elegant solution than the one I wrote. – SomeProgrammer Apr 13 '21 at 19:25
  • 1
    What exactly is your recovery state? The vector to be in its original state? If that s the case, why not `reserve` first, and if that fails, then you know and can throw, otherwise the call's to `push_back` should be good. – NathanOliver Apr 13 '21 at 19:25
  • @NathanOliver they are different vectors... – SomeProgrammer Apr 13 '21 at 19:26
  • 1
    do you really have four different exception types? – user253751 Apr 13 '21 at 19:26
  • @SomeProgrammer So they are. :( well, I guess that's out. – NathanOliver Apr 13 '21 at 19:27
  • @ user253751 This is more of a general question so that I can understand exception handling better, so no this is not real code... – SomeProgrammer Apr 13 '21 at 19:27
  • @SomeProgrammer *" I don't understand why c++ does not have a mechanism to provide a more elegant solution than the one I wrote."* I'm not sure its reasonable to expect the language to offer an out-of-the-box solution. How would you expect it to work? It seems you've noticed how hard this problem is first hand. – François Andrieux Apr 13 '21 at 19:29
  • @FrançoisAndrieux Well they did design RAII for exception handling, didn't they? I thought there was (and probably is) a solution using RAII... – SomeProgrammer Apr 13 '21 at 19:31
  • @SomeProgrammer Reversing the changes made in a function is non-trivial. In this specific case, since you only `push_back` the inverse function is simple. You just have to `pop_back` (capacity changes not withstanding). But consider a function which performs harder to reverse operations like modifying existing elements. There isn't a general purpose answer to this problem other than copy/swap (which isn't always possible, so not completely general). Otherwise you have to implement the exception guarantee by hand according to your unique use case. – François Andrieux Apr 13 '21 at 19:47
  • @ François Andrieux Well yes you are right. But given the inverse of the function applied, is it possible to easily provide strong exception guarantee without overhead on the happy path? – SomeProgrammer Apr 13 '21 at 19:49
  • @SomeProgrammer As far as I know there is no such thing as a zero cost error handling solution. Even "no cost on the successful path" implementations of `try`/`catch` are not actually 100% free, rather they are just not significantly slower and will have a non-zero cost relative to not handling errors at all. Beyond that, what constitutes acceptable overhead is a matter of opinion, or at least a matter of specific use case requirements. I would try to `scope_guard` solution below and see if it is good enough. – François Andrieux Apr 13 '21 at 19:53

4 Answers4

3

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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
3

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.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • I don't understand. I simply pop_back when push_back has succeeded... – SomeProgrammer Apr 13 '21 at 19:44
  • @SomeProgrammer And if the push_back caused the vector to reallocate, then it has been reallocated even after popping the pushed element. Thus the vector is not in the state that it was in prior to calling the function. – eerorika Apr 13 '21 at 19:45
  • Well yes, technically you are right I guess... but suppose I don't hold any iterators/ references to the elements. Can you find a more elegant solution than mine, while still being zero overhead on happy path? – SomeProgrammer Apr 13 '21 at 19:47
  • If capacity invariance and reference invalidation isn't a problem, simply `reserve`ing the required capacity before inserting elements would be a good compromise, assuming the element constructors don't also potentially throw. – François Andrieux Apr 13 '21 at 19:57
1

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.

C.M.
  • 3,071
  • 1
  • 14
  • 33
0

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
   }
};
user253751
  • 57,427
  • 7
  • 48
  • 90
  • Well, your solution is hardly more elegant than mine... :) And this is more of a general question so that I can write strongly exception safe functions elegantly, so I don't have a specific problem with 4 vectors... – SomeProgrammer Apr 13 '21 at 19:40