diff --git a/error_handling.html b/error_handling.html new file mode 100644 index 0000000..3d56416 --- /dev/null +++ b/error_handling.html @@ -0,0 +1,92 @@ + + + + +
The following paper is a good introduction to some of the issues of + writing robust generic components: + +
+ D. Abrahams: ``Exception Safety + in Generic Components'', originally published in M. + Jazayeri, R. Loos, D. Musser (eds.): Generic Programming, Proc. of a + Dagstuhl Seminar, Lecture Notes on Computer Science 1766 ++ +
The simple answer is: ``whenever the semantic and performance + characteristics of exceptions are appropriate.'' + +
An oft-cited guideline is to ask yourself the question ``is this an + exceptional (or unexpected) situation?'' This guideline has an attractive + ring to it, but is usually a mistake. The problem is that one person's + ``exceptional'' is another's ``expected'': when you really look at the + terms carefully, the distinction evaporates and you're left with no + guideline. After all, if you check for an error condition, then in some + sense you expect it to happen, or the check is wasted code. + +
A more appropriate question to ask is: ``do we want stack unwinding + here?'' Because actually handling an exception is likely to be + significantly slower than executing mainline code, you should also ask: + ``Can I afford stack unwinding here?'' For example, a desktop application + performing a long computation might periodically check to see whether the + user had pressed a cancel button. Throwing an exception could allow the + operation to be cancelled gracefully. On the other hand, it would probably + be inappropriate to throw and handle exceptions in the inner loop of + this computation because that would have a significant performance impact. + +
As a developer, if I have violated a precondition of a library I'm + using, I don't want stack unwinding. What I want is a core dump or the + equivalent - a way to inspect the state of the program at the exact point + where the problem was detected. That usually means assert() or + something like it. + +
Sometimes it is neccessary to have resilient APIs which can stand up to + nearly any kind of client abuse, but there is usually a significant cost to + this approach. For example, it usually requires that each object used by a + client be tracked so that it can be checked for validity. If you need that + sort of protection, it can usually be provided as a layer on top of a + simpler API. Beware half-measures, though. An API which promises resilience + against some, but not all abuse is an invitation to disaster. Clients will + begin to rely on the protection and their expectations will grow to cover + unprotected parts of the interface. + +
Note for Windows developers: unfortunately, the native + exception-handling used by most Windows compilers actually throws an + exception when you use assert(). Actually, this is true of other + programmer errors such as segmentation faults and divide-by-zero errors. + One problem with this is that if you use JIT (Just In Time) debugging, + there will be collateral exception-unwinding before the debugger comes up. + Fortunately, there is a simple but little-known workaround, which is to use + the following incantation: + +
+++extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*) +{ + throw; +} +extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*) + = _set_se_translator(straight_to_debugger); ++
© Copyright David Abrahams 2001. Permission to copy, use, modify, + sell and distribute this document is granted provided this copyright notice + appears in all copies. This document is provided "as is" without express or + implied warranty, and with no claim as to its suitability for any purpose. + +
Revised + 17 March, 2001 diff --git a/generic_exception_safety.html b/generic_exception_safety.html new file mode 100644 index 0000000..41e5096 --- /dev/null +++ b/generic_exception_safety.html @@ -0,0 +1,695 @@ + + + + +
Lessons Learned from Specifying Exception-Safety + for the C++ Standard Library + +
David_Abrahams@dragonsys.com
Abstract. This paper represents the knowledge accumulated in + response to a real-world need: that the C++ Standard Template Library + exhibit useful and well-defined interactions with exceptions, the + error-handling mechanism built-in to the core C++ language. It explores the + meaning of exception-safety, reveals surprising myths about exceptions and + genericity, describes valuable tools for reasoning about program + correctness, and outlines an automated testing procedure for verifying + exception-safety. + +
Keywords: exception-safety, exceptions, STL, C++ + +
Informally, exception-safety in a component means that it exhibits + reasonable behavior when an exception is thrown during its execution. For + most people, the term ``reasonable'' includes all the usual + expectations for error-handling: that resources should not be leaked, and + that the program should remain in a well-defined state so that execution + can continue. For most components, it also includes the expectation that + when an error is encountered, it is reported to the caller. + +
More formally, we can describe a component as minimally exception-safe + if, when exceptions are thrown from within that component, its invariants + are intact. Later on we'll see that at least three different levels of + exception-safety can be usefully distinguished. These distinctions can help + us to describe and reason about the behavior of large systems. + +
In a generic component, we usually have an additional expectation of + exception-neutrality, which means that exceptions thrown by a + component's type parameters should be propagated, unchanged, to the + component's caller. + +
Exception-safety seems straightforward so far: it doesn't constitute + anything more than we'd expect from code using more traditional + error-handling techniques. It might be worthwhile, however, to examine the + term from a psychological viewpoint. Nobody ever spoke of + ``error-safety'' before C++ had exceptions. + +
It's almost as though exceptions are viewed as a mysterious + attack on otherwise correct code, from which we must protect ourselves. + Needless to say, this doesn't lead to a healthy relationship with error + handling! During standardization, a democratic process which requires broad + support for changes, I encountered many widely-held superstitions. In order + to even begin the discussion of exception-safety in generic components, it + may be worthwhile confronting a few of them. + +
``Interactions between templates and exceptions are not + well-understood.'' This myth, often heard from those who consider + these both new language features, is easily disposed of: there simply are + no interactions. A template, once instantiated, works in all respects like + an ordinary class or function. A simple way to reason about the behavior of + a template with exceptions is to think of how a specific instantiation of + that template works. Finally, the genericity of templates should not cause + special concern. Although the component's client supplies part of the + operation (which may, unless otherwise specified, throw arbitrary + exceptions), the same is true of operations using familiar virtual + functions or simple function pointers. + +
``It is well known to be impossible to write an exception-safe + generic container.'' This claim is often heard with reference to + an article by Tom Cargill [4] + in which he explores the problem of exception-safety for a generic stack + template. In his article, Cargill raises many useful questions, but + unfortunately fails to present a solution to his problem.1 + He concludes by suggesting that a solution may not be possible. + Unfortunately, his article was read by many as ``proof'' of that + speculation. Since it was published there have been many examples of + exception-safe generic components, among them the C++ standard library + containers. + +
``Dealing with exceptions will slow code down, and templates are + used specifically to get the best possible performance.'' A good + implementation of C++ will not devote a single instruction cycle to dealing + with exceptions until one is thrown, and then it can be handled at a speed + comparable with that of calling a function [7]. + That alone gives programs using exceptions performance equivalent to that + of a program which ignores the possibility of errors. Using exceptions can + actually result in faster programs than ``traditional'' error + handling methods for other reasons. First, a catch block clearly indicates + to the compiler which code is devoted to error-handling; it can then be + separated from the usual execution path, improving locality of reference. + Second, code using ``traditional'' error handling must typically + test a return value for errors after every single function call; using + exceptions completely eliminates that overhead. + +
``Exceptions make it more difficult to reason about a program's + behavior.'' Usually cited in support of this myth is the way + ``hidden'' execution paths are followed during stack-unwinding. + Hidden execution paths are nothing new to any C++ programmer who expects + local variables to be destroyed upon returning from a function: + +
++ ++ErrorCode f( int& result ) // 1 +{ // 2 + X x; // 3 + ErrorCode err = x.g( result ); // 4 + if ( err != kNoError ) // 5 + return err; // 6 + // ...More code here... + return kNoError; // 7 +} ++
In the example above, there is a ``hidden'' call to
+ X::~X()
in lines 6 and 7. Granted, using exceptions, there is
+ no code devoted to error handling visible:
+
+
++ ++int f() // 1 +{ // 2 + X x; // 3 + int result = x.g(); // 4 + // ...More code here... + return result; // 5 +} ++
For many programmers more familiar with exceptions, the second example + is actually more readable and understandable than the first. The + ``hidden'' code paths include the same calls to destructors of + local variables. In addition, they follow a simple pattern which acts + exactly as though there were a potential return statement after each + function call in case of an exception. Readability is enhanced because the + normal path of execution is unobscured by error-handling, and return values + are freed up to be used in a natural way. + +
There is an even more important way in which exceptions can enhance
+ correctness: by allowing simple class invariants. In the first example, if
+ x
's constructor should need to allocate resources, it has no
+ way to report a failure: in C++, constructors have no return values. The
+ usual result when exceptions are avoided is that classes requiring
+ resources must include a separate initializer function which finishes the
+ job of construction. The programmer can therefore never be sure, when an
+ object of class X
is used, whether he is handling a
+ full-fledged X
or some abortive attempt to construct one (or
+ worse: someone simply forgot to call the initializer!)
+
+
A non-generic component can be described as exception-safe in isolation, + but because of its configurability by client code, exception-safety in a + generic component usually depends on a contract between the component and + its clients. For example, the designer of a generic component might require + that an operation which is used in the component's destructor not throw any + exceptions.2 + The generic component might, in return, provide one of the following + guarantees: + +
The basic guarantee is a simple minimum standard for exception-safety to + which we can hold all components. It says simply that after an exception, + the component can still be used as before. Importantly, the preservation of + invariants allows the component to be destroyed, potentially as part of + stack-unwinding. This guarantee is actually less useful than it might at + first appear. If a component has many valid states, after an exception we + have no idea what state the component is in|only that the state is valid. + The options for recovery in this case are limited: either destruction or + resetting the component to some known state before further use. Consider + the following example: + +
++ ++template <class X> +void print_random_sequence() +{ + std::vector<X> v(10); // A vector of 10 items + try { + // Provides only the basic guarantee + v.insert( v.begin(), X() ); + } + catch(...) {} // ignore any exceptions above + // print the vector's contents + std::cout "(" << v.size() << ") "; + std::copy( v.begin(), v.end(), + std::ostream_iterator<X>( std::cout, " " ) ); +} ++
Since all we know about v after an exception is that it is valid, the
+ function is allowed to print any random sequence of X
s.3
+ It is ``safe'' in the sense that it is not allowed to crash, but
+ its output may be unpredictable.
+
+
The strong guarantee provides full + ``commit-or-rollback'' semantics. In the case of C++ standard + containers, this means, for example, that if an exception is thrown all + iterators remain valid. We also know that the container has exactly the + same elements as before the exception was thrown. A transaction that has no + effects if it fails has obvious benefits: the program state is simple and + predictable in case of an exception. In the C++ standard library, nearly + all of the operations on the node-based containers list, set, multiset, + map, and multimap provide the strong guarantee.4). + +
The no-throw guarantee is the strongest of all, and it says that + an operation is guaranteed not to throw an exception: it always completes + successfully. This guarantee is necessary for most destructors, and indeed + the destructors of C++ standard library components are all guaranteed not + to throw exceptions. The no-throw guarantee turns out to be + important for other reasons, as we shall see.5 + +
Inevitably, the contract can get more complicated: a quid pro quo
+ arrangement is possible. Some components in the C++ Standard Library give
+ one guarantee for arbitrary type parameters, but give a stronger guarantee
+ in exchange for additional promises from the client type that no exceptions
+ will be thrown. For example, the standard container operation
+ vector<T>::erase
gives the basic guarantee for
+ any T
, but for types whose copy constructor and copy
+ assignment operator do not throw, it gives the no-throw guarantee.6
+
+
From a client's point-of-view, the strongest possible level of safety
+ would be ideal. Of course, the no-throw guarantee is simply
+ impossible for many operations, but what about the strong guarantee?
+ For example, suppose we wanted atomic behavior for
+ vector<T>::insert
. Insertion into the middle of a vector
+ requires copying elements after the insertion point into later positions,
+ to make room for the new element. If copying an element can fail, rolling
+ back the operation would require ``undoing'' the previous
+ copies...which depends on copying again. If copying back should fail (as it
+ likely would), we have failed to meet our guarantee.
+
+
One possible alternative would be to redefine insert
to
+ build the new array contents in a fresh piece of memory each time, and only
+ destroy the old contents when that has succeeded. Unfortunately, there is a
+ non-trivial cost if this approach is followed: insertions near the end of a
+ vector which might have previously caused only a few copies would now cause
+ every element to be copied. The basic guarantee is a
+ ``natural'' level of safety for this operation, which it can
+ provide without violating its performance guarantees. In fact all of the
+ operations in the library appear to have such a ``natural'' level
+ of safety.
+
+
Because performance requirements were already a well-established part of + the draft standard and because performance is a primary goal of the STL, + there was no attempt to specify more safety than could be provided within + those requirements. Although not all of the library gives the strong + guarantee, almost any operation on a standard container which gives the + basic guarantee can be made strong using the ``make a + new copy'' strategy described above: + +
++ ++template <class Container, class BasicOp> +void MakeOperationStrong( Container& c, const BasicOp& op ) +{ + Container tmp(c); // Copy c + op(tmp); // Work on the copy + c.swap(tmp); // Cannot fail7 +} ++
This technique can be folded into a wrapper class to make a similar + container which provides stronger guarantees (and different performance + characteristics).8 + +
By considering a particular implementation, we can hope to discern a + natural level of safety. The danger in using this to establish requirements + for a component is that the implementation might be restricted. If someone + should come up with a more-efficient implementation which we'd like to use, + we may find that it's incompatible with our exception-safety requirements. + One might expect this to be of no concern in the well-explored domains of + data structures and algorithms covered by the STL, but even there, advances + are being made. A good example is the recent introsort algorithm [6], + which represents a substantial improvement in worst-case complexity over + the well-established quicksort. + +
To determine exactly how much to demand of the standard components, I + looked at a typical real-world scenario. The chosen test case was a + ``composite container.'' Such a container, built of two or more + standard container components, is not only commonly needed, but serves as a + simple representative case for maintaining invariants in larger systems: + +
++ ++// SearchableStack - A stack which can be efficiently searched +// for any value. +template <class T> +class SearchableStack +{ + public: + void push(const T& t); // O(log n) + void pop(); // O(log n) + bool contains(const T& t) const; // O(log n) + const T& top() const; // O(1) + private: + std::set<T> set_impl; + std::list<std::set<T>::iterator> list_impl; +}; ++
The idea is that the list acts as a stack of set iterators: every + element goes into the set first, and the resulting position is pushed onto + the list. The invariant is straightforward: the set and the list should + always have the same number of elements, and every element of the set + should be referenced by an element of the list. The following + implementation of the push function is designed to give the strong + guarantee within the natural levels of safety provided by set and list: + +
++ ++template <class T> // 1 +void SearchableStack<T>::push(const T& t) // 2 +{ // 3 + set<T>::iterator i = set_impl.insert(t); // 4 + try // 5 + { // 6 + list_impl.push_back(i); // 7 + } // 8 + catch(...) // 9 + { // 10 + set_impl.erase(i); // 11 + throw; // 12 + } // 13 +} // 14 ++
What does our code actually require of the library? We need to examine + the lines where non-const operations occur: + +
set_impl
is modified
+ in the process, our invariant is violated. We need to be able to rely on
+ the strong guarantee from set<T>::insert
.
+
+ push_back
fails, but
+ list_impl
is modified in the process, our invariant is
+ violated, so we need to be able to rely on the strong guarantee
+ from list<T>::insert.
+
+ set<T>::erase
.9
+
+ i
to the erase
function: we need the
+ no-throw guarantee from the copy constructor of
+ set<T>::iterator
.
+ I learned a great deal by approaching the question this way during + standardization. First, the guarantee specified for the composite container + actually depends on stronger guarantees from its components (the + no-throw guarantees in line 11). Also, I took advantage of all of + the natural level of safety to implement this simple example. Finally, the + analysis revealed a requirement on iterators which I had previously + overlooked when operations were considered on their own. The conclusion was + that we should provide as much of the natural level of safety as possible. + Faster but less-safe implementations could always be provided as extensions + to the standard components. 10 + +
As part of the standardization process, I produced an exception-safe + reference implementation of the STL. Error-handling code is seldom + rigorously tested in real life, in part because it is difficult to cause + error conditions to occur. It is very common to see error-handling code + which crashes the first time it is executed ...in a shipping product! To + bolster confidence that the implementation actually worked as advertised, I + designed an automated test suite, based on an exhaustive technique due to + my colleague Matt Arnold. + +
The test program started with the basics: reinforcement and
+ instrumentation, especially of the global operators new
and
+ delete
.11Instances of the components (containers and
+ algorithms) were created, with type parameters chosen to reveal as many
+ potential problems as possible. For example, all type parameters were given
+ a pointer to heap-allocated memory, so that leaking a contained object
+ would be detected as a memory leak.
+
+
Finally, a scheme was designed that could cause an operation to throw an
+ exception at each possible point of failure. At the beginning of every
+ client-supplied operation which is allowed to throw an exception, a call to
+ ThisCanThrow
was added. A call to ThisCanThrow
+ also had to be added everywhere that the generic operation being tested
+ might throw an exception, for example in the global operator
+ new
, for which an instrumented replacement was supplied.
+
+
++ ++// Use this as a type parameter, e.g. vector<TestClass> +struct TestClass +{ + TestClass( int v = 0 ) + : p( ThisCanThrow(), new int( v ) ) {} + TestClass( const TestClass& rhs ) + : p( ThisCanThrow(), new int( *rhs.p ) ) {} + const TestClass& operator=( const TestClass& rhs ) + { ThisCanThrow(); *p = *rhs.p; } + bool operator==( const TestClass& rhs ) + { ThisCanThrow(); return *p == *rhs.p; } + ...etc... + ~TestClass() { delete p; } +}; ++
ThisCanThrow
simply decrements a ``throw
+ counter'' and, if it has reached zero, throws an exception. Each test
+ takes a form which begins the counter at successively higher values in an
+ outer loop and repeatedly attempts to complete the operation being tested.
+ The result is that the operation throws an exception at each successive
+ step along its execution path that can possibly fail. For example, here is
+ a simplified version of the function used to test the strong
+ guarantee: 12
+
+
++ ++extern int gThrowCounter; // The throw counter +void ThisCanThrow() +{ + if (gThrowCounter-- == 0) + throw 0; +} + +template <class Value, class Operation> +void StrongCheck(const Value& v, const Operation& op) +{ + bool succeeded = false; + for (long nextThrowCount = 0; !succeeded; ++nextThrowCount) + { + Value duplicate = v; + try + { + gThrowCounter = nextThrowCount; + op( duplicate ); // Try the operation + succeeded = true; + } + catch(...) // Catch all exceptions + { + bool unchanged = duplicate == v; // Test strong guarantee + assert( unchanged ); + } + // Specialize as desired for each container type, to check + // integrity. For example, size() == distance(begin(),end()) + CheckInvariant(v); // Check any invariant + } +} ++
Notably, this kind of testing is much easier and less intrusive with a
+ generic component than with non-generics, because testing-specific type
+ parameters can be used without modifying the source code of the component
+ being tested. Also, generic functions like StrongCheck
above
+ were instrumental in performing the tests on a wide range of values and
+ operations.
+
+
The description of exception-safety in the C++ Standard [1] + is only slightly more formal, but relies on hard-to-read + ``standardese'' and an occasionally subtle web of implication.13 + In particular, leaks are not treated directly at all. It does have the + advantage that it is the standard. + +
The original reference implementation [5] + of the exception-safe STL is an adaptation of an old version of the SGI + STL, designed for C++ compilers with limited features. Although it is not a + complete STL implementation, the code may be easier to read, and it + illustrates a useful base-class technique for eliminating + exception-handling code in constructors. The full test suite [3] + used to validate the reference implementation has been used successfully to + validate all recent versions of the SGI STL, and has been adapted to test + one other vendor's implementation (which failed). As noted on the + documentation page, it also seems to have the power to reveal hidden + compiler bugs, particularly where optimizers interact with + exception-handling code. + +
1 Probably the greatest impediment to a solution + in Cargill's case was an unfortunate combination of choices on his part: + the interface he chose for his container was incompatible with his + particular demands for safety. By changing either one he might have solved + the problem. + +
2 It is usually inadvisable to throw an + exception from a destructor in C++, since the destructor may itself be + called during the stack-unwinding caused by another exception. If the + second exception is allowed to propagate beyond the destructor, the program + is immediately terminated. + +
3 In practice of course, this function would + make an extremely poor random sequence generator! + +
4 It is worth noting that mutating algorithms
+ usually cannot provide the strong guarantee: to roll back a modified
+ element of a range, it must be set back to its previous value using
+ operator=
, which itself might throw. In the C++ standard
+ library, there are a few exceptions to this rule, whose rollback behavior
+ consists only of destruction: uninitialized_copy
,
+ uninitialized_fill
, and uninitialized_fill_n
.
+
+
5 All type parameters supplied by clients of the + C++ standard library are required not to throw from their destructors. In + return, all components of the C++ standard library provide at least the + basic guarantee. + +
6 Similar arrangements might have been made in + the C++ standard for many of the mutating algorithms, but were never + considered due to time constraints on the standardization process. + +
7 Associative containers whose
+ Compare
object might throw an exception when copied cannot use
+ this technique, since the swap function might fail.
+
+
8 This suggests another potential use for the
+ oft-wished-for but as yet unseen container_traits<>
+ template: automated container selection to meet exception-safety
+ constraints.
+
+
9 One might be tempted to surround the erase
+ operation with a try
/catch
block to reduce the
+ requirements on set<T>
and the problems that arise in
+ case of an exception, but in the end that just begs the question. First,
+ erase just failed and in this case there are no viable alternative ways to
+ produce the necessary result. Second and more generally, because of the
+ variability of its type parameters a generic component can seldom be
+ assured that any alternatives will succeed.
+
+
10 The prevalent philosophy in the design of + STL was that functionality that wasn't essential to all uses should be left + out in favor of efficiency, as long as that functionality could be obtained + when needed by adapting the base components. This departs from that + philosophy, but it would be difficult or impossible to obtain even the + basic guarantee by adapting a base component that doesn't already + have it. + +
11 An excellent discussion on how to fortify + memory subsystems can be found in: Steve Maguire, Writing Solid Code, + Microsoft Press, Redmond, WA, 1993, ISBN 1-55615- 551-4. + +
12 Note that this technique requires that the + operation being tested be exception-neutral. If the operation ever tries to + recover from an exception and proceed, the throw counter will be negative, + and subsequent operations that might fail will not be tested for + exception-safety. + +
13 The changes to the draft standard which + introduced exception-safety were made late in the process, when amendments + were likely to be rejected solely on the basis of the number of altered + words. Unfortunately, the result compromises clarity somewhat in favor of + brevity. Greg Colvin was responsible for the clever language-lawyering + needed to minimize the extent of these changes. + diff --git a/index.htm b/index.htm index e7affcf..71367b1 100644 --- a/index.htm +++ b/index.htm @@ -55,19 +55,30 @@ other headers each boost header includes.
+ +Error and Exception Handling + describes approaches to errors and exceptions by David Abrahams. +
Counted Body Techniques by Kevlin Henney is must reading for those interested in reference counting, a widely used object management idiom. Originally published in Overload magazine.
-Generic Programming Techniques - by David Abrahams describes some of - the techniques used in Boost libraries.
+ +Generic Programming + Techniques by David Abrahams + and Jeremy Siekdescribe some of the + techniques used in Boost libraries.
+Feature Model Diagrams in text and HTML describes how to represent feature model diagrams in text form.
+Portability Hints: Borland C++ 5.5.1 describes Borland C++ portability issues, with suggested workarounds.
+Portability Hints: Microsoft VC++ 6.0 SP4 describes Microsoft C++ portability issues, with suggested workarounds.
+