pinDesigning C++ Interfaces - Exception Safety

Overload Journal #43 - Jun 2001 + Programming Topics   Author: Mark Radford

In software design, there is so much which can be achieved without considering the implementation language, and therefore which can be expressed in a modelling language - UML for example - usually with textual augmentation. However, this only goes so far, and when it comes to specifics, the implementation language exerts a great influence on how the design of an interface is approached.

The term Modern C++ refers to the C++ of the ISO standard, which has been around now for almost three years, and this is a language which has moved on dramatically from the C++ described in older texts, for example the ARM [E-S]. The evolutionary factors which influence interface design fall into two categories:

  • New features introduced into the language during the standardisation process, the dominant examples being exception safety and templates.

  • The experience gained over the years bringing about maturity in the ideas on how interface design is approached, an example of this being the role played by freestanding functions.

This article concentrates on exception safety, presenting some examples of how writing exception safe code can seriously affect the design of participating components.

A Simple Example

It is a mistake to think the presence of exceptions only affects implementation - nothing could be further from the truth. Exceptions - at least in the form they take in C++ - impact on the design of every interface!

By way of an illustrative example, consider the stack class design shown in the following fragment:

template <typename T> class stack
{
public:
  T pop();
//...
};

The above approach is typical of how a stack might have been designed in days past, and is indeed reminiscent of a design presented by Carroll & Ellis (in [Carroll-1995], page 112). Now, consider one typical way in which this might be used:

void f(stack<my_type>& s)
{
  my_type mt;
// ...  
  mt = s.pop();
}

The problem is this: if an exception is thrown, for example during the assignment, the element on the top of the stack will be lost forever, and with this interface there is no other way around this problem - an alternative approach is required.

Consider an alternative design, and a typical way of using it (analogous to the one shown above), in which the query and pop operations are separated (as in the stack adapter class of the C++ standard library):

template <typename T> class stack
{
public:
  T const& top() const;
  void pop();
//...
};
void f(stack<my_type>& s)
{
  my_type mt;
// ...
  mt = s.top();
  s.pop();
}

What makes this example particularly relevant is that it illustrates how, in the presence of exceptions, we need to beware of preconceived ideas when designing interfaces. With the latter approach, if the assignment throws, the state of the stack is not affected, but although it now looks like we have a solution, appearances can be deceptive and there is a price to pay: separating the top() and pop() functions means the design is not thread safe.

To illustrate what can go wrong in a multithreaded environment, consider the example in the following fragment:

void f(stack<my_type>& s)
{
  s.pop();            // #1
  my_type mt(s.top);  // #2
}

If, following #1, another thread makes a call to pop(), the value read in #2 may not - and indeed is unlikely to be - the one expected. The pop() and top() calls need to be placed in a synchronised piece of code (i.e. either critical sectioned or mutexed).

Exception Safety Guarantees

So far, this article has discussed exception safety without clarifying what that means. It is time to set the record straight. The meaning of the term exception safety is crystallised in the following guarantees (originally formulated by Dave Abrahams):

  • The basic guarantee - if an operation throws an exception, no resources will leak as a result and invariants (of the component on which the operation is invoked) are preserved

  • The strong guarantee - additionally, the program's state remains unchanged

  • The nothrow guarantee - an operation guarantees never to propagate an exception under any circumstances

The basic and strong guarantees represent the two levels of exception safety. The purpose of the nothrow guarantee is to enable the basic and strong guarantees to be honoured.

Normally, it is the strong guarantee which requires operations promising nothrow, except that destructors must always honour this guarantee (because the destructor might be called as the result of an exception being thrown, with highly undesirable results).

The basic guarantee is relatively straightforward to honour: provided resources are managed using the "Execute Around Object" [Henney2000] idiom (a.k.a. "Resource Acquisition is Initialisation" [Stroustrup1997]) - this way, the C++ practice of placing finalisation code in destructors ensures that whichever route an operation exits by, cleanup will be performed.

Honouring the strong guarantee is a bit trickier, and more often requires the assistance of the nothrow guarantee...

The Role of the Nothrow Guarantee

To illustrate the role of the nothrow guarantee making it possible to honour the strong exception safety guarantee, consider the case of inserting elements into std::vector, as in the following code fragment:

template <typename type> void f_unsafe(std::vector<type>& v)
{
// ...
  v.insert(v.end(), first, last);
// ...
}

Inserting into containers can result in an exception being thrown in two ways:

  • bad_alloc might be thrown if there is a need to increase the vector's capacity and the attempt to allocate memory fails, and

  • the generic nature of the code means we must assume it is possible for type's copy constructor might throw an exception.

It is the second of these which causes the problem, because an exception might be thrown when some but not all insertions have already completed, v is left in an indeterminate state.

The procedure for manipulating containers with exception safety in C++ forms a simple and recurring pattern: create a copy of the original, manipulate the working copy, then swap the working copy back into the original, as illustrated in the next code fragment:

template <typename type> void f_safe(std::vector<type>& v)
{
  std::vector<type> v_temp(v);
  v_temp.insert(v.end(), first, last);
  v.swap(v_temp);
// ...
}

The new state of v - that is, the state following the insertions - is constructed in the working copy v_temp. No operations have been applied to v at this point, therefore if at any time during the insertions into v_temp, an exception is thrown, the control flow exits from f_safe leaving the state of v unchanged. Next, vector::swap() is invoked, swapping over the respective contents of v and v_temp; the working copy v_temp now holds the original state, and is destructed in due course.

This procedure is possible because - and only because - swap() supports the nothrow guarantee, so the transition in v's state can be made without any danger of an exception being thrown.

This whole procedure is rather long winded and inefficient - there is the overhead of copying the vector. This is necessary because the nature of vector is such that it is not possible to perform an insert without the danger that an exception might be thrown.

If the container in the above example is changed to list then the code becomes:

template <typename type> void f_list(std::list<type>& l)
{
  l.insert(l.end(), first, last);
// ...
}

It is noteworthy that node based containers such as list, can offer the capability to insert elements with a guarantee that the operation will either succeed or will leave the container unchanged (that is, the strong guarantee is supported), because the insert operation can construct the new nodes, and then splice them in. Construction of the new nodes takes place before any operations are applied to the original, so if an exception is thrown during this process the state of the original is not affected. Splicing in the newly constructed nodes simply involves reassignment of pointers - an operation which will not throw, i.e. the operation carries a nothrow guarantee.

If the strong guarantee is to be supported, then the additional performance overhead caused by the fact that some containers must be copied when inserting elements, becomes a factor which must be considered when choosing a container.

Summary

The understanding that exception safety affects the design of interfaces is a major step towards achieving exception safety - an assertion which hopefully this article has gone some way towards substantiating by means of the examples and explanations presented.

For example, to achieve exception safety it was necessary to modify the interface of the stack class, although in doing so thread safety must become an issue for the client code (a design tradeoff - you can't have your cake and eat it).

Another example was that of inserting elements into a vector, where supporting the strong exception safety guarantee was possible only because vector's interface offers a swap() function supporting the nothrow guarantee. This was then contrasted with node based containers such as list, which can offer an insert() operation which naturally supports the strong guarantee.

Writing exception safe code in C++ seemed very difficult at one time, but now seems less so largely because the techniques for doing so - as well as the tradeoffs involved - are much better understood within the community.

References

[Carroll-1995] Martin D Carroll and Margaret A Ellis, "Designing and Coding Reusable C++", Addison-Wesley, 1995.

[E-S] Margaret A. Ellis and Bjarne Stroustrup, "The Annotated C++ Reference Manual", Addison-Wesley 1990.

[Henney2000] Kevlin Hennery, "C++ Patterns: Executing Around Sequences" (www.curbralan.com)

[Stroustrup1997] Bjarne Stroustrup, "C++ Programming Language", 3rd edition, Addison-Wesley, 1997.

Overload Journal #43 - Jun 2001 + Programming Topics