Solution

I l @ ve RuBoard

graphics/bulb.gif

Is-Implemented-In-Terms-Of

By coining expressions like Has-A, Is-A, and Uses-A, we have developed a convenient shorthand for describing many types of code relationships.

"Is-A," or more precisely "Is-Substitutable-For-A," must follow Barbara Liskov's Substitutability Principle (LSP), in which she defines what it means for a type S to be substitutable for a type T:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T . [Liskov88]

Is-A is usually used to describe public inheritance that preserves substitutability according to the LSP, as all public inheritance ought to do. For example, " D Is-A B " means that code that accepts objects of the base class B by pointer or reference can seamlessly use objects of the publicly derived class D instead.

It's important to remember, however, that there are more ways to spell "Is-A" in C++ than are dreamt of in inheritance alone. Is-A can also describe unrelated (by inheritance) classes that support the same interface and can therefore be used interchangeably in templated code that uses that common interface. The LSP applies to this form of substitutability just as much as it does to other forms. In this context, for example, " X Is-A Y " ”or, " X Is-Substitutable-For-A Y " ”communicates that templated code that accepts objects of type Y will also accept objects of type X because both X and Y support the same interface. For an enjoyable treatise on substitutability, a good place to begin is Kevlin Henney's article "Substitutability" [Henney00].

Clearly, both kinds of substitutability depend on the context in which the objects are actually used, but the point is that Is-A can be implemented in different ways. So, as it turns out, can Is-Implemented-In-Terms-Of, as we shall now see.

1. What does Is-Implemented-In-Terms-Of mean?

An equally common code relationship is Is-Implemented-In-Terms-Of, or IIITO for short. A type T IIITO another type U if T uses U in its implementation in some form. Saying "uses in some form" certainly leaves a lot of latitude, and 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 , as shown in Example 23-1(a):

 // Example 23-1(a): "T IIITO U" using Has-A // class T {   // ... private:   U* u_;  // or by value or by reference }; 

or that T is derived from U nonpublicly, as shown in Example 23-1(b): [14]

[14] Arguably, public derivation also models IIITO, but the primary meaning of public derivation is still Is-Substitutable-For-A.

 // Example 23-1(b): "T IIITO U" using derivation // class T : private U {   // ... }; 

This brings us to the natural questions: When we have a choice, which is the better way to implement IIITO? What are the trade-offs? When should we consider using each one?

How to Implement IIITO: Inheritance or Delegation?

2. 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 inherit privately from U or to contain a U member object.

As I've argued before, inheritance is often overused , even by experienced developers. A sound rule of software engineering is to minimize coupling. If a relationship can be expressed in more than one way, use the weakest relationship that's practical. Given that inheritance is nearly the strongest relationship we can express in C++, second only to friendship, [15] it's only really appropriate when there is no equivalent weaker alternative. If you can express a class relationship using delegation alone, you should always prefer that.

[15] A friend of a class X has the strongest possible relationship because it has access to and can depend upon all the members of X . A class derived from X only has access to and can only depend upon X 's public and protected members.

The principle of minimum coupling clearly has a direct effect on the robustness (or fragility) of your code, of how long your compile times are, and other observable consequences. What's interesting is that the choice between inheritance and delegation for IIITO turns out to have exception safety implications. In hindsight, that the principle of minimum coupling should also relate to exception safety should not be surprising, because a design's coupling has a direct impact on its possible exception safety.

The coupling principle states:

Lower coupling promotes program correctness (including exception safety), and tight coupling reduces the maximum possible program correctness (including exception safety) .

This is only natural. After all, the less tightly real-world objects are related , the less effect they 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 let's return to Examples 23-1(a) and 23-1(b) and consider again a class T that IIITO another type U . Consider the copy assignment operator: How does the choice of how to express the IIITO relationship affect how we write T::operator=() ?

Exception Safety Consequences

Does the choice between these techniques have exception safety implications? Explain . (Ignore any issues not related to exception safety . )

First, consider how we would have to write T::operator=() if the IIITO relationship is expressed using Has-A. We of course have the good habit of using the common "do all the work off to the side, then commit using nonthrowing operations only" technique to maximize exception safety, and so we would write something like the following:

 // Example 23-2(a): "T IIITO U" 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; } 

This is pretty good. Without making any assumptions about U , we can write a T::operator=() that is "nearly" strongly exception-safe except for possible side effects of U .

Even if the U object were contained by value instead of by pointer, it could be easily transformed to be held by pointer as above. The U object could also be put into a Pimpl using the transformation described in the first part of this Item. It is precisely the fact that delegation (Has-A) gives us this flexibility that allows us to easily write a fairly exception-safe T::operator=() without making any assumptions about U .

Next, consider how the problem changes once the relationship between T and U involves any kind of inheritance:

 // Example 23-2(b): "T IIITO U" 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 alluded to in the Item 22 sidebar (speaking of a similar case), 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 do so for U::operator=() ?)

In other words, now T 's ability to make an exception safety guarantee for its own member function T::operator=s() depends implicitly on U 's own safety and guarantees . Again, should this be surprising? No, because Example 23-2(b) uses the tightest possible relationship, and hence the highest possible coupling, to express the connection between T and U .

Summary

Looser 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 Exceptional C++ [Sutter00] Item 24 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, only use inheritance when delegation alone won't suffice.

I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net