2

I've been thinking about how to implement the various exception safety guarantee, especially the the strong guarantee, i.e. data is rolled back to it's original state when an exception occurs.

Consider the following, wonderfully contrived examples (C++11 code). Suppose there is a simple data structure storing some value

struct Data
{
  int value = 321;
};

and some function modify() operating on that value

void modify(Data& data, int newValue, bool throwExc = false)
{
  data.value = newValue;

  if(throwExc)
  {
    // some exception occurs, sentry will roll-back stuff
    throw std::exception();
  }
}

(one can see how contrived this is). Suppose we wanted to offer the strong exception-safety guarantee for modify(). In case of an exception, the value of Data::value is obviously not rolled back to its original value. One could naively go ahead and try the whole function, setting back stuff manually in appropriate catch block, which is enormously tedious and doesn't scale at all.

Another approach is to use some scoped, RAII helper - sort of like a sentry which knows what to temporarily save and restore in case of an error:

struct FakeSentry
{
  FakeSentry(Data& data) : data_(data), value_(data_.value)
  {
  }

  ~FakeSentry()
  {
    if(!accepted_)
    {
      // roll-back if accept() wasn't called
      data_.value = value_;
    }
  }

  void accept()
  {
    accepted_ = true;
  }

  Data& data_ ;
  int   value_;

  bool accepted_ = false;
};

The application is simple and require to only call accept() in case of modify() succeeding:

void modify(Data& data, int newValue, bool throwExc = false)
{
  FakeSentry sentry(data);
  data.value = newValue;

  if(throwExc)
  {
    // some exception occurs, sentry will roll-back stuff
    throw std::exception();
  }

  // prevent rollback
  sentry.accept();
}

This gets the job done but doesn't scale well either. There would need to be a sentry for each distinct user-defined type, knowing all the internals of said type.

My question now is: What other patterns, idioms or preferred courses of action come to mind when trying to implement strongly exception safe code?

thokra
  • 2,874
  • 18
  • 25
  • 6
    I think the easiest is to have no-throw swap functions, do the throwing operations on copies, and then swap the modified copy in. – R. Martinho Fernandes Oct 30 '13 at 13:06
  • You could always make the sentry a template class on the value type you want to roll back. I did that for a project here and it works okay, as long as all the necessary operators are supplied. – arne Oct 30 '13 at 13:09
  • @R.MartinhoFernandes: That's cool, but what if copying is not feasible because of performance degradation due to object complexity? Granted, using some other way may not alleviate that problem at atll. However, for many, many cases, this is probably suffiencient. – thokra Oct 30 '13 at 13:22
  • @arne: For simple types adhering to some layout and interface convention that's ok. – thokra Oct 30 '13 at 13:22
  • 1
    If such a copy is so expensive, how can you cheaply store enough information to undo the action? (note how there's also a copy needed in your FakeSentry example). Also, I'm not saying it's never possible, just pointing out that if you have an object with expensive copies, your hands are likely tied in any case. Sometimes the strong guarantee is too costly to be worth it, and people will stick to the basic guarantee (the object is still usable). It's a cost-benefit thing. – R. Martinho Fernandes Oct 30 '13 at 13:24
  • @R.MartinhoFernandes: As I said, performance-wise you may not gain anything by using some other approach. Thanks for the sound advice! – thokra Oct 30 '13 at 13:30
  • @R.MartinhoFernandes One of examples is vector: suppose we have vector `x` with many elements inside, and we are adding 20 elements - it is easy to do `x.resize(original_size);` on failure. But making copy of full vector just to add 20 elements and then swap - can be very expensive. – Evgeny Panasyuk Oct 30 '13 at 13:40
  • Are you sure that is good enough? Because neither vector::push_back nor vector::insert provide the strong guarantee. – R. Martinho Fernandes Oct 30 '13 at 13:46
  • 1
    @R.MartinhoFernandes: `std::vector::push_back` does - at least according to http://en.cppreference.com/w/cpp/container/vector/push_back. `resize()` on the other hand is `noexcept` if the new size is smaller than the current size. Therefore, if you want to push back 20 elements and, say, 19 succeed and the last one throws, resizing should actually roll-back correctly and destroy the already added 19 elements. If one of the destructors throws ... damn... :) – thokra Oct 30 '13 at 13:58
  • The standard says otherwise. It explicitly excludes T's copy constructor from the "no effects" guarantee. "If an exception is thrown *other than by the copy constructor, move constructor, assignment operator, or move assignment operator of `T`* or by any InputIterator operation there are no effects". I don't know why, but now I'm intrigued. (And there will be a copy of T for `push_back(const T&)`) – R. Martinho Fernandes Oct 30 '13 at 13:59
  • @R.MartinhoFernandes: Yes, assuming all those are conceptually `noexcept`, the strong guarantee applies, right? Why does the standard have to formulate stuff so incredibly backwards sometimes. – thokra Oct 30 '13 at 14:04
  • That's not a valid assumption. Copy ctors that can throw are far from uncommon (e.g. `std::vector`'s). – R. Martinho Fernandes Oct 30 '13 at 14:05
  • @EvgenyPanasyuk If you are looking for strong exception safety, you would make a copy of the original vector, add the 20 new items to it, and swap the copy with the original. Doing something else would (e.g. modifying the original) would not get you strong exception safety (that does not mean, however, that it would be *unsafe* - it just wouldn't meet the criteria for strong exception safety). – Zac Howland Oct 30 '13 at 15:06
  • @thokra: Destructors should never throw: http://bin-login.name/ftp/pub/docs/programming_languages/cpp/cffective_cpp/MAGAZINE/SU_FRAME.HTM#destruct – Zac Howland Oct 30 '13 at 15:09
  • @ZacHowland: I'm aware (that's why I added the "damn"). Still, thanks! – thokra Oct 30 '13 at 15:11
  • @ZacHowland 1. Why do you think `push_back`'ing elements to existing vector does not have strong exception safety? Suppose we resize back to original on failure. 2. There are cases when it is perfectly safe to throw from destructor. – Evgeny Panasyuk Oct 30 '13 at 15:14
  • @EvgenyPanasyuk `push_back` requires the ability to allocate a memory block (which can throw), copy the existing elements into the new block (which can throw), and copy the new items into the extra space in the block (which can throw). If you are doing that on the original vector, any one of those throws has changed the state of the original. Even resizing may not return the state of the original vector to what it was before (suppose the copy failed on an element that already existed?). – Zac Howland Oct 30 '13 at 15:24
  • @EvgenyPanasyuk And it is never *perfectly safe* to throw from a destructor. "More Effective C++", Item 11: Never let an exception escape a destructor. See the link (posted previously) for Herb Sutter's explanation of why doing so is **evil**, and can actually cause a memory leak (among other issues). – Zac Howland Oct 30 '13 at 15:25
  • @R.MartinhoFernandes Check 23.2.1/10: "*if an exception is thrown by a push_back() or push_front() function, that function has no effects.*" that was before your quote. So putting all together - it is not safe, if you only have `non-CopyInsertable T` which throws from destructor. – Evgeny Panasyuk Oct 30 '13 at 15:26
  • @ZacHowland If exception happens during reallocation - it will just destruct already copied elements, and revert to original storage. If you have enough capacity, and during push_back you get exception - it will just restore original size. – Evgeny Panasyuk Oct 30 '13 at 15:29
  • @ZacHowland I know all arguments why not to throw from destructor. And I know special cases when it is perfectly legal thing to do. Just never say never. – Evgeny Panasyuk Oct 30 '13 at 15:30
  • @EvgenyPanasyuk You do realize it is **not** just me saying to never throw from a destructor ... it is 2 guys that sit on the standards committee. So yes, you can say **never**. – Zac Howland Oct 30 '13 at 15:36
  • @ZacHowland: [nitpicking]If you're referring to Scott Meyers, I'm afraid he has never been part of the standardization committee.[/nitpicking] – thokra Oct 30 '13 at 15:38
  • @ZacHowland I read works of of Alexandrescu, Abrahams, Mayers, Sutter and many others on topic of exception safety. There are cases when it is legal to throw from destructor. For instance, read this article by Abrahams and Kalb: ["Evil, or Just Misunderstood?"](http://cpp-next.com/archive/2012/08/evil-or-just-misunderstood/) - it is exactly about throwing destructor. Also, check [this example](http://ideone.com/9M91MB) - there is throw from destructor and nothing bad has happened. – Evgeny Panasyuk Oct 30 '13 at 15:41
  • @thokra: Herb Sutter was the Secretary of the committee for the C++93 and C++03 standards (IIRC, he was replaced by PJ Plauger for C++11). While Scott was not on the committee itself, he was, and is, very close with the committee members. – Zac Howland Oct 30 '13 at 15:43
  • @ZacHowland: Yes, but Scott also stresses very often that he *is not* a committee member. ;) Also, Abrahams and Kalb are quite the authorities as well. You may want reconsider. – thokra Oct 30 '13 at 15:47
  • @EvgenyPanasyuk That article just talks about a theoretical way you could handle it (and completely misses the basic case: `new T[]; delete [] T;` when `T::~T()` throws). And your example is doing nothing useful, so it is simply masking the *real* problems with throwing from a destructor. You are effectively taking Charlie's disclaimer (http://stackoverflow.com/questions/391595/when-is-it-ok-to-throw-an-exception-from-a-destructor-in-c) and asserting that you might possibly find a use for it. – Zac Howland Oct 30 '13 at 15:53
  • @thokra Abrahams and Kalb state that it is theoretically possible to do something useful with it (their justification for removing the "evil" label), but don't offer any real situation where you should (or even can) do something useful by doing so. Basically, their argument is that we shouldn't force destructors to be `noexcept` by default because we *might* find something we can do with it later. (it is `noexcept` by default, btw). – Zac Howland Oct 30 '13 at 15:56
  • @ZacHowland The example above implements guard which is analog to `scope(success)` feature from `D` language. You can put `file.close()` to it or any other deferred action into branch of destructor. And it shows situation in which you can legally throw from destructor. I am not saying that everyone should throw everywhere, I just showing that there are perfectly legal use-cases and it is not *100% Evil*. – Evgeny Panasyuk Oct 30 '13 at 15:57
  • @ZacHowland Article is not about `noexcept`, but it is about `std::terminate` rules: "*So there you have it. The reason we can’t have throwing destructors is that nobody worked out how to deal with multiple exceptions wanting to propagate through the same set of stack frames. Considering the fact that the program knows how to unwind from here, even if it doesn’t know exactly what to propagate, and the fact that it’s so easy to throw from a destructor by mistake, we think **termination** is a bit draconian.*". – Evgeny Panasyuk Oct 30 '13 at 16:11
  • @EvgenyPanasyuk You are playing with semantics a bit (if something is 99.9% evil, does that no longer make it evil? ;) My point about the article was that was their argument for not making destructors `noexcept` and preventing you from being able to change it. The case (IIRC, Herb mentioned it during the 03 discussions, and PJ brought it back up for 11) was being made to prevent all destructors from throwing **and** to prevent it from being something that could be overridden. Abrahams and Kalb used the argument in that article as a case for leaving the override capability. – Zac Howland Oct 30 '13 at 16:19

3 Answers3

3

In general it is called ScopeGuard idiom. It is not always possible to use temporary variable and swap to commit (though it is easy when acceptable) - sometime you need to modify existing structures.

Andrei Alexandrescu and Petru Marginean discuss it in details in following paper: "Generic: Change the Way You Write Exception-Safe Code — Forever".


There is Boost.ScopeExit library which allows to make guard code without coding auxiliary classes. Example from documentation:

void world::add_person(person const& a_person) {
    bool commit = false;

    persons_.push_back(a_person);           // (1) direct action
    // Following block is executed when the enclosing scope exits.
    BOOST_SCOPE_EXIT(&commit, &persons_) {
        if(!commit) persons_.pop_back();    // (2) rollback action
    } BOOST_SCOPE_EXIT_END

    // ...                                  // (3) other operations

    commit = true;                          // (4) disable rollback actions
}

D programming language has special construct in language for that purpose - scope(failure)

Transaction abc()
{
    Foo f;
    Bar b;

    f = dofoo();
    scope(failure) dofoo_undo(f);

    b = dobar();

    return Transaction(f, b);
}:

Andrei Alexandrescu shows advantages of that language construct in his talk: "Three Unlikely Successful Features of D"


I have made platform dependent implementation of scope(failure) feature which works on MSVC, GCC, Clag and Intel compilers. It is in library: stack_unwinding. In C++11 it allows to achieve syntax which is very close to D language. Here is Online DEMO:

int main()
{
    using namespace std;
    {
        cout << "success case:" << endl;
        scope(exit)
        {
            cout << "exit" << endl;
        };
        scope(success)
        {
            cout << "success" << endl;
        };
        scope(failure)
        {
            cout << "failure" << endl;
        };
    }
    cout << string(16,'_') << endl;
    try
    {
        cout << "failure case:" << endl;
        scope(exit)
        {
            cout << "exit" << endl;
        };
        scope(success)
        {
            cout << "success" << endl;
        };
        scope(failure)
        {
            cout << "failure" << endl;
        };
        throw 1;
    }
    catch(int){}
}

Output is:

success case:
success
exit
________________
failure case:
failure
exit
Evgeny Panasyuk
  • 9,076
  • 1
  • 33
  • 54
3

The usual approach is not to roll back in case of an exception, but to commit in case of no exception. That means, do the critical stuff first in a way that does not necessarily alter program state, and then commit with a series of non-throwing actions.

Your example would be done like follows then:

void modify(Data& data, int newValue, bool throwExc = false)
{ 
  //first try the critical part
  if(throwExc)
  {
    // some exception occurs, sentry will roll-back stuff
    throw std::exception();
  }

  //then non-throwing commit
  data.value = newValue;
}

Of course RAII plays a major role in exception safety, but it's not the only solution.
Another example for "try-and-commit" is the copy-swap-idiom:

X& operator=(X const& other) {
  X tmp(other);    //copy-construct, might throw
  tmp.swap(*this); //swap is a no-throw operation
}

As you can see, this sometimes comes at the cost of additional actions (e.g. if C's copy ctor allocates memory), but that's the price you have to pay some times for exceptionsafety.

Arne Mertz
  • 24,171
  • 3
  • 51
  • 90
  • The first approach works - but only if possibly throwing operations do not depend on stuff that needs to be changed beforehand and may subsequently have to be rolled back, i.e. non-throwing op A -> throwing op B depending on op A -> rollback A if B throws. Still interesting. – thokra Oct 30 '13 at 13:18
  • It is common practice to use `X& operator=(X other) { swap(*this, other); return *this; }` (note, taking by value, not by `const&` like in your code). It can take advantage of copy/move elision in some cases, and as the result will be faster. Refer paper: ["Want Speed? Pass by Value."](http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/) – Evgeny Panasyuk Oct 30 '13 at 13:31
  • @thokra yes. But as Robert's comment to your question suggests, you can often do `A` to a temporary. Say, non-throwing op A' -> throwing op B depending on A' -> commit A' to A if succeeded. Often the throwing operations are very basic and/or provide the strong gurantee themselves. RAII and rollback techniques are mostly necessary if there is more than one possibly throwing operation. – Arne Mertz Oct 30 '13 at 13:43
  • @ArneMertz: Sure, that'll do. Thanks! – thokra Oct 30 '13 at 13:58
0

I found this question when faced with the case at the end.

If you want to ensure the commit-or-rollback semantics without using copy-and-swap I would recommend providing proxies for all objects and using the proxies consistently.

The idea would be to hide the implementation details and limit the operations on the data to a sub-set that can be rolled back efficiently.

So the code using the data-structure would be something like this:

void modify(Data&data) {
   CoRProxy proxy(data);
   // Only modify data through proxy - DO NOT USE data
   ... foo(proxy);
   ...
   proxy.commit(); // If we don't reach this point data will be rolled back
}

struct Data {
  int value;
  MyBigDataStructure value2; // Expensive to copy
};

struct CoRProxy {
  int& value;
  const MyBigDataStructure& value2; // Read-only access

  void commit() {m_commit=true;}
  CoRProxy(data&d):value(d.value),value2(d.value2),
      m_commit(false),m_origValue(d.value){;}
  ~CoRProxy() {if (!m_commit) std::swap(m_origValue,value);}
private:
  bool m_commit;
  int m_origValue;
};

The main point is that the proxy restricts the interface to data to operations that the proxy can roll back, and (optionally) provides read-only access to the rest of the data. If we really want to ensure that there is no direct access to data we can send the proxy to a new function (or use a lambda).

A similar use-case is using a vector and rolling back push_back in case of failure.

template <class T> struct CoRVectorPushBack {
   void push_back(const T&t) {m_value.push_back(t);}
   void commit() {m_commit=true;}

   CoRVectorPushBack(std::vector<T>&data):
    m_value(data),m_origSize(data.size()),m_commit(false){;}

   ~CoRVectorPushBack() {if (!m_commit) value.resize(m_origSize);}

private:
   std::vector<T>&m_value;
   size_t m_origSize;
   bool m_commit;
};

The downside of this is the need for making a separate class for each operation. The upside is that the code using the proxies is straightforward and safe (we could even add if (m_commit) throw std::logic_error(); in push_back).

Hans Olsson
  • 11,123
  • 15
  • 38