Programming 版 (精华区)

发信人: jinqiao (我·哎·你), 信区: Programming
标  题: Programming with Exceptions
发信站: 哈工大紫丁香 (2001年09月07日18:27:07 星期五), 站内信件

by Bjarne Stroustrup, the Author of C++ Programming Language, The: Special E
dition
APR 06, 2001

This article presents two series of examples of motivating the Standard C++ 
notion of a basic guarantee of exception safety, and shows how the technique
s required to provide that basic guarantee actually lead to simpler programs
.

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 t
o 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. Interest
ingly, thoughts about exception safety often lead to simpler and more manage
able code.
In this article, I first present concepts and techniques for managing resour
ces and for designing classes in a program relying on exceptions. For this p
resentation, I use the simplest examples that I can think of. Finally, I exp
lain how these ideas are directly reflected in the C++ standard library so t
hat you can immediately benefit from them.
Resources and Resource Leaks
Consider a traditional piece of code:
void use_file(const char* fn)
{
        FILE* f = fopen(fn,"r");
        // use f
        fclose(f);
}
This code looks plausible. However, if something goes wrong after the call o
f fopen() and before the call of fclose(), it's possible to exit use_file() 
without calling fclose(). In particular, an exception might be thrown in the
 use f code, or in a function called from there. Even an ordinary return cou
ld bypass fclose(f), but that's more likely to be noticed by a programmer or
 by testing.
A typical first attempt to make use_file() fault-tolerant looks like this:
void use_file(const char* fn)
{
        FILE* f = fopen(fn,"r");
        try {
                // use f
        }
        catch (...) {
                fclose(f);
                throw;
        }
        fclose(f);
}
The code using the file is enclosed in a try block that catches every except
ion, closes the file, and re-throws the exception.
The problem with this solution is that it's ad hoc, verbose, tedious, and po
tentially expensive. Another problem is that the programmer has to remember 
to apply this solution everywhere a file is opened, and must get it right ev
ery time. Such ad hoc solutions are inherently error-prone. Fortunately, the
re is a more elegant solution.
It's a fundamental rule that when a variable goes out of scope its destructo
r is called. This is true even if the scope is exited by an exception. There
fore, 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 File_ptr that acts like 
a FILE*:
class File_ptr {
        FILE* p;
public:
        File_ptr(const char* n, const char* a) { p = fopen(n,a); }
        // suitable copy operations
        ~File_ptr() { if (p) fclose(p); }
        operator FILE*() { return p; }   // extract pointer for use
};
Given that, our function shrinks to this minimum:
void use_file(const char* fn)
{
        File_ptr f(fn,"r");
        // use f
}
The destructor will be called independently of whether the function is exite
d normally or exited because an exception is thrown. That is, the exception-
handling mechanism enables us to remove the error-handling code from the mai
n algorithm. The resulting code is simpler and less error-prone than its tra
ditional counterpart.
The file example is a fairly ordinary resource leak problem. A resource is a
nything that our code acquires from somewhere and needs to give back. A reso
urce that is not properly "given back'' (released) is said to be leaked. Oth
er examples of common resources are memory, sockets, and thread handles. Res
ource management is the heart of many programs. Typically, we want to make s
ure than every resource is properly released, whether we use exceptions or n
ot.
You could say that I have merely shifted the complexity away from the use_fi
le() function into the File_ptr class. That's true, but I need only write th
e File_ptr once for a program, and I often open files more frequently than t
hat. In general, to use this technique we need one small "resource handle cl
ass'' for each kind of resource in a system. Some libraries provide such cla
sses for the resources they offer, so the application programmer is saved th
at task.
The C++ standard library provides auto_ptr for holding individual objects. I
t also provides containers, notably vector and string, for managing sequence
s of objects.
The technique of having a constructor acquire a resource and a destructor re
lease it is usually called resource acquisition is initialization.
Class Invariants
Consider a simple vector class:
class Vector {
        // v points to an array of sz ints
        int sz;
        int* v;
public:
        explicit Vector(int n);           // create vector of n ints
        Vector(const Vector&);
        ~Vector();                        // destroy vector
        Vector& operator=(const Vector&); // assignment
        int size() const;
        void resize(int n);               // change the size to n
        int& operator[](int);             // subscripting
        const int& operator[](int) const; // subscripting
};
A class invariant is a simple rule, devised by the designer of the class, th
at must hold whenever a member function is called. This Vector 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 i
nvariant holds when they're called. In return, they must make sure that the 
invariant holds when they return. For example:
int Vector::size() const { return sz; }
This implementation of size() looks clean enough, and it is. The invariant g
uarantees that sz really does hold the number of elements, and since size() 
doesn't change anything, the invariant is maintained.
The subscript operation is slightly more involved:
struct Bad_range { };
int& Vector::operator[](int i)
{
        if (0<=i && i<sz) return v[i];
        trow Bad_range();
}
That is, if the index is in range, return a reference to the right element; 
otherwise, throw an exception of type Bad_range.
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 be
come quite messy. But how can they rely on the invariant? Because constructo
rs establish it. For example:
Vector::Vector(int i) :sz(i), v(new int[i]) { }
In particular, note that if new throws an exception, no object will be creat
ed. It's therefore impossible to create a Vector that doesn't hold the reque
sted elements.
The key idea of the preceding section was that we should avoid resource leak
s. So, clearly, Vector needs a destructor that frees the memory acquired by 
a Vector:
Vector::~Vector() { delete[] v; }
Again, the reason that this destructor can be so simple is that we can rely 
on v pointing to allocated memory.
Now consider a naive implementation of assignment:
Vector& Vector::operator=(const Vector& a)
{
        sz = a.sz;              // get new size
        delete[] v;             // free old memory
        v = new int[n];         // get new memory
        copy(a.v,a.v+a.sz,v);   // copy to new memory
}
People who have 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:
int main()
try
{
        Vector vec(10);
        cout << vec.size() << '\n';   // so far, so good
        Vector v2(40*1000000);         // ask for 160 megabytes
        vec = v2;                      // use another 160 megabytes
}
catch(Range_error) {
        cerr << "Oops: Range error!\n";
}
catch(bad_alloc) {
        cerr << "Oops: memory exhausted!\n";
}
If you hope for a nice error message Oops: 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 v2 will fail in a controlled manner, produc
ing that expected error message. However, if you have 160MB, but not 320MB (
as I do on my laptop), that's not going to happen. When the assignment tries
 to allocate memory for the copy of the elements, a bad_alloc exception is t
hrown. The exception handling then tries to exit the block in which vec is d
efined. In doing so, the destructor is called for vec, and the destructor tr
ies to deallocate vec.v. However, operator=() has already deallocated that a
rray. Some memory managers take a dim view of such (illegal) attempts to dea
llocate the same memory twice. One system went into an infinite loop when so
meone deleted the same memory twice.
What really went wrong here? The implementation of operator=() failed to mai
ntain the class invariant 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 pro
blem 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 a
way before you have an alternative:
Vector& Vector::operator=(const Vector& a)
{
        int* p = new int[n];    // get new memory
        copy(a.v,a.v+a.sz,p);   // copy to new memory
        sz = a.sz;              // get new size
        delete[] v;             // free old memory
        v = p;
}
Now, if new fails to find memory and throws an exception, the vector being a
ssigned will simply remain unchanged. In particular, our example above will 
exit with the correct error message: Oops: memory exhausted!.
Please note that Vector is an example of a resource handle; it manages its r
esource (the element array) simply and safely through the resource acquisiti
on is initialization technique described earlier.
Exception Safety
The notions of resource management and invariants allow us to formulate the 
basic exception safety guarantee of the C++ standard library. Simply put, we
 can't consider any class exception safe unless it has an invariant and main
tains it even when exceptions occur. Furthermore, we can't consider any piec
e of code to be exception-safe unless it properly releases all resources it 
acquired.
Thus, the standard library provides this guarantee:
Basic guarantee for all operations: The basic invariants of the standard lib
rary are maintained, and no resources, such as memory, are leaked.
The standard library further defines these guarantees:
Strong guarantee for key operations: In addition to providing the basic guar
antee, either the operation succeeds, or has no effects. This guarantee is p
rovided for key library operations, such as push_back(), and single-element 
insert() on a list.
Nothrow guarantee for some operations: In addition to providing the basic gu
arantee, some operations are guaranteed not to throw an exception. This guar
antee is provided for a few simple operations, such as swap() and freeing me
mory.
These concepts are invaluable when thinking about exception safety. Trying t
o add enough try-blocks to a program to deal with every problem is simply to
o messy, too complicated, and can easily lead to inefficient code. Structuri
ng code as described earlier, 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=() actually provides the s
trong guarantee. Often the strong guarantee comes naturally when you try not
 to delete an old representation before you've constructed a new one. The ba
sic guarantee is used more when you're optimizing code to avoid having to du
plicate information.
More Information
You can find a much more exhaustive discussion of exception safety and techn
iques for writing exception-safe code in Appendix E, "Standard-Library Excep
tion Safety," in The C++ Programming Language, Special Edition (Addison-Wesl
ey, 2000, ISBN 0-201-70073-5), here abbreviated TC++PL for simplicity. If yo
u have a version of TC++PL without that appendix, you can download a copy of
 the appendix from my home pages at http://www.research.att.com/~bs.
If you're not acquainted with exceptions in C++, I strongly recommend that y
ou learn about them and their proper use. Used well, exceptions can signific
antly simplify code. Naturally, I recommend TC++PL, but any modern C++ book—
:meaning one that's written to take advantage of the ISO C++ standard and it
s standard library—:should have an explanation.
If you're not yet comfortable with standard library facilities such as strin
g and vector, I strongly encourage you to try them. Code that directly messe
s around with memory management and elements in arrays is among the most pro
ne to resource leaks and nasty exception-safety problems. Such code is rarel
y systematic and the data structures involved rarely have simple and useful 
invariants. A very brief introduction to basic standard library facilities c
an be found in Chapter 3 of TC++PL, "A Tour of the Standard Library.'' That,
 too, can be downloaded from my home pages.
Copyright (c) 2001 Bjarne Stroustrup

--


   I Believe I can Fly!
          I Believe I can Touch The Sky!

※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.118.236.162]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:209.040毫秒