Channels ▼
RSS

C/C++

Transactional Programming


Calum, who holds a Ph.D. in Computer Science from the University of Cambridge, is a senior software engineer at Sophos Plc. He can be contacted at calum.grant@sophos.com.


When programs encounter exceptions, they must not leave data in an inconsistent state. It would be no good for a function to half-complete because that could leave the data in an unexpected state and lead to bugs later in the program. But this is exactly what can happen when exceptions are thrown halfway through a function.

For example, the problem with this function:


std::vector<std::string> list1, list2;
void f(const std::string &s)
{
 list1.push_back(s);
 list2.push_back(s);
}

is that the second push_back() might fail and throw std::bad_alloc. This would leave the program in an inconsistent state because list1 and list2 are different. While desktop computers rarely deny a memory request, std::bad_alloc should be anticipated on portable devices or servers.

In an ideal world, functions should either completely succeed or fail without side effects. This all-or-nothing behavior is referred to as "atomic" because it will never leave the program in a halfway state.

A way of describing the effects of exceptions on functions is with Abrahams guarantees [1]:

  • Basic Guarantee. If an exception is thrown, no resources are leaked and objects remain in a destructible and usable—but not necessarily predictable—state.
  • Strong Guarantee. If an exception is thrown, the program state remains unchanged.
  • Nothrow Guarantee. The function will not emit an exception under any circumstances.

Although the nothrow guarantee sounds ideal, it is often impossible to arrange for a function to never throw an exception, especially when memory allocation takes place, which could throw std::bad_alloc at an inopportune moment. Many standard library containers rely on memory allocation and could, therefore, throw. The basic guarantee is often not good enough, since leaving a program in a safe but indeterminate state is not terribly useful. It is much better to live with exceptions, and let them propagate out to the caller in a safe and predictable way.

But error recovery is not straightforward in C++. When an exception is thrown, all of the side effects prior to the exception must be undone. In real code this is not often done, or it is added as an afterthought. Even when exceptions are considered up front, analyzing them and recovering from them is difficult and error prone. Exceptional paths are generally the least tested part of software—it is impractical to unit-test every exceptional path caused by std::bad_alloc.

While the best solution is to try to redesign the data structures, this isn't always possible. Consequently, you might try to catch the second exception and undo the push_back() on list1:


void f(const std::string &s)
{
 list1.push_back(s);
 try
 {
  list2.push_back(s);
 }
 catch(...)
 {
  list1.pop_back();
  throw;
 }
}

This code is safe because the Standard guarantees that pop_back() will not throw an exception. Unfortunately, it is also ugly. In more complex situations, wrapping individual operations in a try-catch is hard work and makes the code difficult to read.

Scope-guards [2] offer an alternative. A scope-guard is a utility class that performs some important clean-up in its destructor, as in:

void f(const std::string &s)
{
  list1.push_back(s);
  ScopeGuard guard = MakeObjGuard(
   list1, &std::vector<std::string>::pop_back);
  list2.push_back(s);
  guard.Dismiss();
}

guard calls list1.pop_back() in its destructor, unless guard.Dismiss() is called. So the scope-guard automatically undoes list1.push_back() if list2.push_back() fails. This is an improvement because there is no need to write out the exception handler.

Unfortunately, while you can safely undo a push_back(), you cannot safely undo a deletion or a push_front() because an exception could be thrown while trying to clean up after the first exception. For example:


void g()
{
  list.pop_back();
  h(list);
}

You could not write:

void g()
{
 std::string saved_back = 
    list.back();
 list.pop_back();
 try
 {
  h(list);
 }
 catch(...)
 {
  list.push_back(saved_back);
                  // Can throw
  throw;
 }
}

because list.push_back() could itself fail, breaking our guarantee of atomic behavior.

This shows that only certain operations can be undone on the standard containers. Atomic behavior is only possible to guarantee when a function has no side effects prior to an exception being thrown, or the availability of a guaranteed undo function that can be called in a scope-guard or an exception handler.

Dealing with exceptions is complicated and sometimes messy, and requires different approaches in different circumstances. Some good examples of the subtleties of exception handling are given by Herb Sutter both in Exceptional C++: 47 Engineering Puzzles, Programming Problems and Solutions (Addison-Wesley, 2000); and More Exceptional C++ (Addison-Wesley, 2002).


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video