eh_brief
eh_brief
Bjarne Stroustrup
AT&T Labs - Research
ABSTRACT
This article presents two series of examples motivating Standard C++’s notion of a
basic guarantee of exception safety, and shows how the techniques required to provide
that basic guarantee actually lead to simpler programs.
1 Introduction
One of the nice things about Standard C++ is that you can use exceptions for systematic error handling.
However, when you take that approach, you have to take care that when an exception is thrown, it doesn’t
cause more problems than it solves. That is, you have to think about exception safety. Interestingly,
thoughts about exception safety often lead to simpler and more manageable code.
Here, I first present concepts and techniques for managing resources and for designing classes in a pro-
gram relying on exceptions. For this presentation, I use the simplest examples that I can think of. Finally I
explain how these ideas are directly reflected in the C++ standard library so that you can immediately bene-
fit from them.
The problem with this solution is that it is ad hoc, verbose, tedious, and potentially expensive. The
problem is that the programmer has to remember to apply this solution everywhere a file is opened, and
must get it right every time. Such ad hoc solutions are inherently error-prone. Fortunately, there is a more
elegant solution.
It is a fundamental rule that when a variable goes out of scope its destructor is called. This is true even
if the scope is exited by an exception. Therefore, if we can get a destructor for a local variable to close the
file, we have a solution.
For example, we can define a class FFiillee__ppttrr that acts like a F
FIIL
LEE*:
ccllaassss FFiillee__ppttrr {
FFIILLE E* pp;
ppuubblliicc:
FFiillee__ppttrr(ccoonnsstt cchhaarr* nn, ccoonnsstt cchhaarr* aa) { p = ffooppeenn(nn,aa); }
// suitable copy operations
˜FFiillee__ppttrr() { iiff (pp) ffcclloossee(pp); }
ooppeerraattoorr F
FIIL
LEE*() { rreettuurrnn pp; } // extract pointer for use
};
Given that, our function shrinks to this minimum:
vvooiidd uussee__ffiillee(ccoonnsstt cchhaarr* ffnn)
{
Fiillee__ppttrr ff(ffnn,"rr");
F
// use f
}
The destructor will be called independently of whether the function is exited normally or exited because an
exception is thrown. That is, the exception-handling mechanism enables us to remove the error-handling
code from the main algorithm. The resulting code is simpler and less error-prone than its traditional coun-
terpart.
The file example is a fairly ordinary resource leak problem. A resource is anything that our code
acquires from somewhere and needs to give back. A resource that is not properly ‘‘given back’’ (released)
is said to be leaked. Other examples of common resources are memory, sockets, and thread handles.
Resource management is the heart of many programs. Typically, we want to make sure than every resource
is properly released, whether we use exceptions or not.
You could say that I have merely shifted the complexity away from the uussee__ffiillee() function into the
Fiillee__ppttrr class. That’s true, but I need only write the F
F Fiillee__ppttrr once for a program, and I often open files
more often than that. In general, to use this technique we need one small ‘‘resource handle class’’ for each
kind of resource in a system. Some libraries provide such classes for the resources they offer, so the appli-
cation programmer is saved that task.
The C++ standard library provides aauuttoo__ppttrr for holding individual objects. It also provides containers,
notably vveeccttoorr and ssttrriinngg, for managing sequences of objects.
The technique of having a constructor acquire a resource and a destructor release it is usually called
‘‘resource acquisition is initialization.’’
3 Class Invariants
Consider a simple vector class:
ccllaassss V Veeccttoorr {
// v points to an array of sz ints
iinntt sszz;
iinntt* vv;
ppuubblliicc:
eexxpplliicciitt V
Veeccttoorr(iinntt nn); // create vector of n ints
VVeeccttoorr(ccoonnsstt V Veeccttoorr&);
˜V Veeccttoorr(); // destroy vector
VVeeccttoorr& ooppeerraattoorr=(ccoonnsstt VVeeccttoorr&); // assignment
iinntt ssiizzee() ccoonnsstt;
vvooiidd rreessiizzee(iinntt nn); // change the size to n
iinntt& ooppeerraattoorr[](iinntt ii); // subscripting
ccoonnsstt iinntt& ooppeerraattoorr[](iinntt ii) ccoonnsstt; // subscripting
};
A class invariant is a simple rule, devised by the class’ designer, that must hold whenever a member func-
tion is called. This V
Veeccttoorr class has the simple invariant ‘‘v points to an array of sz ints.’’ All functions are
written with the assumption that this is true. That is, they can assume that this invariant holds when they
are called. In return, they must make sure that the invariant holds when they return. For example:
iinntt V
Veeccttoorr::ssiizzee() ccoonnsstt { rreettuurrnn sszz; }
This implementation of ssiizzee() looks clean enough, and it is. The invariant guarantees that sszz really does
hold the number of elements, and since ssiizzee() doesn’t change anything, the invariant is maintained.
The subscript operation is slightly more involved:
Baadd__rraannggee { };
ssttrruucctt B
iinntt& V
Veeccttoorr::ooppeerraattoorr[](iinntt ii)
{
iiff (00<=ii && ii<sszz) rreettuurrnn vv[ii];
tthhrroow
w BBaadd__rraannggee();
}
That is, if the index is in range return a reference to the right element; otherwise throw a exception of type
Baadd__rraannggee.
B
These functions are simple because they rely on the invariant ‘‘v points to an array of sz ints.’’ Had
they not been able to do that, the code could have become quite messy. But how can they rely on the
invariant? Because constructors establish it. For example:
V
Veeccttoorr::V
Veeccttoorr(iinntt ii) :sszz(ii), vv(nneew
w iinntt[ii]) { }
In particular, note that if nneew w throws an exception, no object will be created. It is therefore impossible to
create a V
Veeccttoorr that doesn’t hold the requested elements.
The key idea of section 2 was that we should avoid resource leaks. So clearly V Veeccttoorr needs a destructor
that frees the memory acquired by a V Veeccttoorr:
V
Veeccttoorr::˜V
Veeccttoorr() { ddeelleettee[] vv; }
Again, the reason this destructor can be so simple is that we can rely on v pointing to allocated memory.
Now consider a naive implementation of assignment:
V
Veeccttoorr& V Veeccttoorr::ooppeerraattoorr=(ccoonnsstt V
Veeccttoorr& aa)
{
sszz = aa.sszz; // get new size
ddeelleettee[] vv; // free old memory
v = nneew w iinntt[nn]; // get new memory
ccooppyy(aa.vv,aa.vv+aa.sszz,vv); // copy to new memory
}
People with experience with exceptions will look at this assignment with suspicion. Can an exception be
thrown? If so, is the invariant maintained?
Actually, this assignment is a disaster waiting to happen:
iinntt mmaaiinn()
ttrryy
{
VVeeccttoorr vveecc(1100);
ccoouutt << vveecc.ssiizzee() << ´\\nn´; // so far, so good
VVeeccttoorr vv22(4400*11000000000000); // ask for 160 megabytes
vveecc = vv22; // use another 160 megabytes
}
ccaattcchh(R Raannggee__eerrrroorr) {
cceerrrr << "O Oooppss: RRaannggee eerrrroorr!\\nn";
}
ccaattcchh(bbaadd__aalllloocc) {
cceerrrr << "O Oooppss: mmeemmoorryy eexxhhaauusstteedd!\\nn";
}
If you hope for a nice error message "O Oooppss:memory exhausted!" because you don’t have 320MB to spare,
you might be disappointed. If you don’t have (about) 160MB free, the construction of vv22 will fail in a con-
trolled manner, producing that expected error message. However, if you have 160MB, but not 320MB (as I
do on my laptop), that is not going to happen. When the assignment tries to allocate memory for the copy of
the elements, a bbaadd__aalllloocc exception is thrown. The exception handling then tries to exit the block where
vveecc is defined. In doing so, the destructor is called for vveecc, and the destructor will try to deallocate vveecc.vv.
However, ooppeerraattoorr=() has already deallocated that array. Some memory managers take a dim view on
such (illegal) attempts to deallocate the same memory twice. One system went into an infinite loop when
did that.
What really went wrong here? The implementation of ooppeerraattoorr=() failed to maintain the class invari-
ant ‘‘v points to an array of sz ints.’’ That done, it was just a matter of time before some disaster happened.
Once we phrase the problem that way, fixing it is easy: Make sure that the invariant holds before throwing
an exception. Or, even simpler: Don’t throw a good representation away before you have an alternative:
V
Veeccttoorr& V Veeccttoorr::ooppeerraattoorr=(ccoonnsstt V
Veeccttoorr& aa)
{
iinntt* p = nneew w iinntt[nn]; // get new memory
ccooppyy(aa.vv,aa.vv+aa.sszz,pp); // copy to new memory
sszz = aa.sszz; // get new size
ddeelleettee[] vv; // free old memory
v = pp;
}
Now, if nneew
w fails to find memory and throws an exception, the vector being assigned to will simply remain
unchanged. In particular, our example above will exit with the correct error message: "O Oooppss:memory
exhausted!".
Please note that V Veeccttoorr is an example of a resource handle; it manages its resource (the element array)
simply and safely through the ‘‘resource acquisition is initialization’’ technique described in section 2.
4 Exception Safety
The notions of resource management and invariants allow us to formulate the basic exception safety guar-
antee of the C++ standard library. Simply put, we can’t consider any class exception safe unless it has an
invariant and maintains it even when exceptions occur. Furthermore, we cannot consider any piece of code
exception safe unless it properly releases all resources it acquired.
Thus, the standard library provides the
‘‘Basic guarantee for all operations:’’ The basic invariants of the standard library are maintained, and
no resources, such as memory, are leaked.
The standard library further defines the
‘‘Strong guarantee for key operations:’’ In addition to providing the basic guarantee, either the opera-
tion succeeds, or has no effects. This guarantee is provided for key library operations, such as
ppuusshh__bbaacckk(), and single-element iinnsseerrtt() on a lliisstt.
and the
‘‘Nothrow guarantee for some operations:’’ In addition to providing the basic guarantee, some
operations are guaranteed not to throw an exception This guarantee is provided for a few simple opera-
tions, such as ssw
waapp() and freeing memory.
These concepts are invaluable when it comes to thinking about exceptions safety. Trying to add enough
try-blocks to a program to deal with every problem is simply too messy, too complicated, and can easily
lead to inefficient code. Structuring code as indicated in sections 2 and 3, with the aim of providing the
strong guarantee where possible and the basic guarantee always, is easier and leads to more maintainable
code. Note that the Vector::operator=() in section 3 actually provides the strong guarantee. Often the
strong guarantee comes naturally when you try not to delete an old representation before you have con-
structed a new one. The basic guarantee is used more when you are optimizing code to avoid having to
duplicate information.
5 More Information
You can find a much more exhaustive discussion of exception safety and techniques for writing exception
safe code in Appendix E: "Standard-Library Exception Safety" in "The C++ Programming Language (Spe-
cial Edition)". If you have an version TC++PL without that appendix, you can download a copy of that
appendix from my home pages.
If you are not acquainted with exceptions in C++, I strongly recommend that you learn about them and
their proper use. Used well, exceptions can significantly simplify code. Naturally, I recommend TC++PL,
but any modern C++ book – meaning one that is written to take advantage of the ISO C++ standard and its
standard library – should have an explanation.
If you are not yet comfortable with standard library facilities, such as ssttrriinngg and vveeccttoorr, I strongly
encourage you to try them. Code that directly messes around with memory management and elements in
arrays is among the most prone to resource leaks and nasty exception-safety problems. Such code is rarely
systematic and the data structures involved rarely have simple and useful invariants. A very brief introduc-
tion to basic standard library facilities can be found in Chapter 3 of TC++PL: ‘‘A Tour of the Standard
Library.’’ That too can be downloaded from my home pages.