This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard. |
Exception-Safe Class Design, Part 2: Inheritance Difficulty: 7 / 10What does IS-IMPLEMENTED-IN-TERMS-OF mean? It may surprise you to learn that there are definite exception-safety consequences when choosing between inheritance and delegation. Can you spot them? ProblemJG Question1. What does IS-IMPLEMENTED-IN-TERMS-OF mean? Guru Question2. In C++, IS-IMPLEMENTED-IN-TERMS-OF can be expressed by either nonpublic inheritance or by containment/ delegation. That is, when writing a class T that is implemented in terms of a class U, the two main options are to either inherit privately from U or to contain a U member object. Does the choice between these techniques have exception safety implications? Explain. (Ignore any issues not related to exception safety.) SolutionIS-IMPLEMENTED-IN-TERMS-OF1. What does IS-IMPLEMENTED-IN-TERMS-OF mean? A type T is IS-IMPLEMENTED-IN-TERMS-OF (IIITO) type U if T uses U in its implementation in some form. This can run the gamut from T being an adapter or proxy or wrapper for U, to T simply using U incidentally to implement some details of T's own services. Typically "T IIITO U" means that either T HAS-A U: // Example 1(a): IIITO using HAS-A
//
class T
{
// ...
private:
U* u_; // or by value or by reference
}; or that T is derived from U nonpublicly: // Example 1(b): IIITO using derivation
//
class T : private U
{
// ...
}; Arguably, public derivation also models IIITO incidentally, but the primary meaning of public derivation is IS-A (in the sense of LSP, IS-SUBSTITUTABLE-FOR-A). Inheritance vs. Delegation2. In C++, IS-IMPLEMENTED-IN-TERMS-OF can be expressed by either nonpublic inheritance or by containment/ delegation. That is, when writing a class T that is implemented in terms of a class U, the two main options are to either inherit privately from U or to contain a U member object. As I've argued before:[1] [2] "Inheritance is often overused, even by experienced developers. Always minimize coupling: If a class relationship can be expressed in more than one way, use the weakest relationship that's practical. Given that inheritance is nearly the strongest relationship you can express in C++ (second only to friendship), it's only really appropriate when there is no equivalent weaker alternative." "If you can express a class relationship using containment alone, you should always prefer that. If you need inheritance but aren't modeling [Liskov] IS-A, use nonpublic inheritance."
It turns out that the above "minimize coupling" principle also relates to exception safety, because a design's coupling has a direct impact on its possible exception safety. Exception Safety ConsequencesDoes the choice between these techniques have exception safety implications? Explain. (Ignore any issues not related to exception safety.) The short answer is this: Lower coupling promotes program correctness (including exception safety), and tight coupling reduces the maximum possible program correctness (including exception safety). This state of affairs shouldn't be surprising. After all, the less tightly real-world objects are related, the less effect they necessarily have on each other. That's why we put firewalls in buildings and bulkheads in ships; if there's a failure in one compartment, the more we've isolated the compartments the less likely the failure is to spread to other compartments before things can be brought back under control. Now consider against a class T that is IIITO another type U. How does the choice of how to express the IIITO relationship affect how we write T::operator=()? First, consider HAS-A: // Example 2(a): IIITO using HAS-A
//
class T
{
// ...
private:
U* u_;
}; T& T::operator=( const T& other )
{
U* temp = new U( *other.u_ ); // do all the work
// off to the side
delete u_; // then "commit" the work using
u_ = temp; // nonthrowing operations only
return *this;
} Therefore we can write a "nearly" strongly exception-safe T::operator=()... and we haven't made any assumptions at all about U. (See the next GotW for more about what "nearly strongly exception-safe" actually means.) Even if the U object were contained by value instead of by pointer, it could be easily transformed into being held by pointer as above. The U object could also be put into a Pimpl using the transformation described in GotW #59. It is precisely the fact that containment (HAS-A) gives us this flexibility that allows us to easily write an exception-safe T::operator=() without making any assumptions about U. Consider next how the problem changes once the relationship between T and U involves any kind of inheritance: // Example 2(b): IIITO using derivation
//
class T : private U
{
// ...
}; T& T::operator=( const T& other )
{
U::operator=( other ); // ???
return *this;
} The problem is the call to U::operator=(). As noted in GotW #59, if U::operator=() can throw in such a way that it has already started to modify the target, there is no way to write a strongly exception-safe T::operator=() unless U provides suitable facilities through some other function (but if U can do that, why doesn't it for U::operator=()?). In other words, now T's ability to make an exception safety guarantee for its T::operator=() depends implicitly on U's own safety and guarantees. But, really, should this surprise us? No, it shouldn't, because Example 2(b) uses the tightest possible relationship, and hence the highest possible coupling, between T and U. SummaryLower coupling promotes program correctness (including exception safety), and tight coupling reduces the maximum possible program correctness (including exception safety). Inheritance is often overused, even by experienced developers. See the references[1] [2] for more information about many other reasons (besides exception safety) why and how you should use delegation instead of inheritance wherever possible. Always minimize coupling: If a class relationship can be expressed in more than one way, use the weakest relationship that's practical. In particular, never use inheritance except where containment/delegation won't suffice. Notes1. H. Sutter. "Uses and Abuses of Inheritance, Part 1" and "Uses and Abuses of Inheritance, Part 2" (C++ Report, October 1998 and January 1999). 2. H. Sutter. Exceptional C++, Item 24 (Addison-Wesley, 2000) |