This would surely be an improvement because function declarations would be more explicit. Unfortunately, you can’t always know by looking at the code in a function whether an exception will be thrown—it could happen because of a memory allocation, for example. Worse, existing functions written before exception handling was introduced may find themselves inadvertently throwing exceptions because of the functions they call (which might be linked into new, exception-throwing versions). Hence, the uninformative situation whereby .
void f();
means, "Maybe I’ll throw an exception, maybe I won’t." This ambiguity is necessary to avoid hindering code evolution. If you want to specify that fthrows no exceptions, use the empty list, as in: .
void f() throw();
Exception specifications and inheritance
Each public function in a class essentially forms a contract with the user; if you pass it certain arguments, it will perform certain operations and/or return a result. The same contract must hold true in derived classes; otherwise the expected "is-a" relationship between derived and base classes is violated. Since exception specifications are logically part of a function’s declaration, they too must remain consistent across an inheritance hierarchy. For example, if a member function in a base class says it will only throw an exception of type A, an override of that function in a derived class must not add any other exception types to the specification list, because that would result in unexpected exceptions for the user, breaking any programs that adhere to the base class interface. You can, however, specify fewer exceptions or none at all , since that doesn’t require the user to do anything differently. You can also specify anything that "is-a" Ain place of Ain the derived function’s specification. Here’s an example .
// C01:Covariance.cpp
// Compile Only!
//{-msc}
#include
using namespace std;
class Base {
public:
class BaseException {};
class DerivedException : public BaseException {};
virtual void f() throw (DerivedException) {
throw DerivedException();
}
virtual void g() throw (BaseException) {
throw BaseException();
}
};
class Derived : public Base {
public:
void f() throw (BaseException) {
throw BaseException();
}
virtual void g() throw (DerivedException) {
throw DerivedException();
}
};
A compiler should flag the override of Derived::f( )with an error (or at least a warning) since it changes its exception specification in a way that violates the specification of Base::f( ). The specification for Derived::g( )is acceptable because DerivedException"is-a" BaseException(not the other way around). You can think of Base/Derivedand BaseException/DerivedExceptionas parallel class hierarchies; when you are in Derived, you can replace references to BaseExceptionin exception specifications and return values with DerivedException. This behavior is called covariance (since both sets of classes vary down their respective hierarchies together). (Reminder from Volume 1: parameter types are not covariant—you are not allowed to change the signature of an overridden virtual function.) .
When not to use exception specifications
If you peruse the function declarations throughout the Standard C++ library, you’ll find that not a single exception specification occurs anywhere! Although this might seem strange, there is a good reason for this seeming incongruity: the library consists mainly of templates, and you never know what a generic might do. For example, suppose you are developing a generic stack template and attempt to affix an exception specification to your pop function, like this:
T pop() throw(logic_error);
Since the only error you anticipate is a stack underflow, you might think it’s safe to specify a logic_erroror some other appropriate exception type. But since you don’t know much about the type T, what if its copy constructor could possibly throw an exception (it’s not unreasonable, after all)? Then unexpected( )would be called, and your program would terminate. The point is that you shouldn’t make guarantees that you can’t stand behind. If you don’t know what exceptions might occur, don’t use exception specifications. That’s why template classes, which constitute 90 percent of the Standard C++ library, do not use exception specifications—they specify the exceptions they know about in documentation and leave the rest to you. Exception specifications are mainly for non-template classes .
In Chapter 7 we’ll take an in-depth look at the containers in the Standard C++ library, including the stack container. One thing you’ll notice is that the declaration of the pop( )member function looks like this:
void pop();
You might think it strange that pop( )doesn’t return a value. Instead, it just removes the element at the top of the stack. To retrieve the top value, call top( )before you call pop( ). There is an important reason for this behavior, and it has to do with exception safety , a crucial consideration in library design .
Suppose you are implementing a stack with a dynamic array (we’ll call it dataand the counter integer count), and you try to write pop( )so that it returns a value. The code for such a pop( )might look something like this:
template
T stack::pop() {
if (count == 0)
throw logic_error("stack underflow");
else
return data[--count];
}
What happens if the copy constructor that is called for the return value in the last line throws an exception when the value is returned? The popped element is not returned because of the exception, and yet counthas already been decremented, so the top element you wanted is lost forever! The problem is that this function attempts to do two things at once: (1) return a value, and (2) change the state of the stack. It is better to separate these two actions into two separate member functions, which is exactly what the standard stackclass does. (In other words, follow the time-worn design practice of cohesion —every function should do one thing well .) Exception-safe code leaves objects in a consistent state and does not leak resources .
You also need to be careful writing custom assignment operators. In Chapter 12 of Volume 1, you saw that operator=should adhere to the following pattern:
1. Make sure you’re not assigning to self. If you are, go to step 6. (This is strictly an optimization.)
2. Allocate new memory required by pointer data members.
3. Copy data from the old memory to the new.
4. Delete the old memory.
5. Update the object’s state by assigning the new heap pointers to the pointer data members.
6. Return *this.
It’s important to not change the state of your object until all the new pieces have been safely allocated and initialized. A good technique is to move all of steps 2 and 3 into a separate function, often called clone( ). The following example does this for a class that has two pointer members, theStringand theInts .
Читать дальше