I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

Are you wondering why a question like this gets a title like "Name Lookup?A class="docLink" HREF="0201615622_part03.html#part03">Part 3"? If so, you'll soon see why, as we consider an application of the Interface Principle discussed in the previous Item.

What Does a Class Depend On?

"What's in a class?" isn't just a philosophical question. It's a fundamentally practical question, because without the correct answer, we can't properly analyze class dependencies.

To demonstrate this, consider a seemingly unrelated problem: What's the best way to write operator<< for a class? There are two main ways, both of which involve tradeoffs. I'll analyze both. In the end we'll find that we're back to the Interface Principle and that it has given us important guidance to analyze the tradeoffs correctly.

Here's the first way:



//*** Example 5 (a) -- nonvirtual streaming 


class X


{


  /*...ostream is never mentioned here...*/


};


ostream& operator<<( ostream& o, const X& x )


{


  /* code to output an X to a stream */


  return o;


}


Here's the second:



//*** Example 5 (b) -- virtual streaming 


class X


{


  /*...*/


public:


  virtual ostream& print( ostream& ) const;


};


ostream& X::print( ostream& o ) const


{


  /* code to output an X to a stream */


  return o;


}


ostream& operator<<( ostream& o, const X& x )


{


  return x.print( o );


}


Assume that in both cases the class and the function declaration appear in the same header and/or namespace. Which one would you choose? What are the tradeoffs? Historically, experienced C++ programmers have analyzed these options this way:

  • Option (a)'s advantage (we've said until now) is that X has fewer dependencies. Because no member function of X mentions ostream, X does not (appear to) depend on ostream. Option (a) also avoids the overhead of an extra virtual function call.

  • Option (b)'s advantage is that any DerivedX will also print correctly, even when an X& is passed to operator<<.

This is the traditional analysis. Alas, this analysis is flawed. Armed with the Interface Principle, we can see why: The first advantage in Option (a) is a phantom, as indicated by the comments in italics.

  1. According to the Interface Principle, as long as operator<< both "mentions" X (true in both cases) and is "supplied with" X (true in both cases), it is logically part of X.

  2. In both cases, operator<< mentions ostream, so operator<< depends on ostream.

  3. Because, in both cases, operator<< is logically part of X and operator<< depends on ostream, therefore in both cases, X depends on ostream.

So what we've traditionally thought of as Option (a)'s main advantage is not an advantage at all. In both cases, X still in fact depends on ostream anyway. If, as is typical, operator<< and X appear in the same header X.h, then both X's own implementation module and all client modules that use X physically depend on ostream and require at least its forward declaration in order to compile.

With Option (a)'s first advantage exposed as a phantom, the choice really boils down to just the virtual function call overhead. Without applying the Interface Principle, though, we would not have been able to as easily analyze the true dependencies (and therefore the true tradeoffs) in this common real-world example.

Bottom line, it's not always useful to distinguish between members and nonmembers, especially when it comes to analyzing dependencies, and that's exactly what the Interface Principle implies.

Some Interesting (and Even Surprising) Results

In general, if A and B are classes and f(A,B) is a free function:

  • If A and f are supplied together, then f is part of A, so A depends on B.

  • If B and f are supplied together, then f is part of B, so B depends on A.

  • If A, B, and f are supplied together, then f is part of both A and B, so A and B are interdependent. This has long made sense on an instinctive level梚f the library author supplies two classes and an operation that uses both, the three are probably intended to be used together. Now, however, the Interface Principle has given us a way to more clearly state this interdependency.

Finally, we get to the really interesting case. In general, if A and B are classes and A::g(B) is a member function of A:

  • Because A::g(B) exists, clearly A always depends on B. No surprises so far.

  • If A and B are supplied together, then of course A::g(B) and B are supplied together. Therefore, because A::g(B) both "mentions" B and is "supplied with" B, then according to the Interface Principle, it follows (perhaps surprisingly, at first) that A::g(B) is part of B, and because A::g(B) uses an (implicit) A* parameter, B depends on A. Because A also depends on B, this means that A and B are interdependent.

At first, it might seem like a stretch to consider a member function of one class as also part of another class, but this is true only if A and B are also supplied together. Consider: If A and B are supplied together (say, in the same header file) and A mentions B in a member function like this, "gut feel" already usually tells us A and B are probably interdependent. They are certainly strongly coupled and cohesive, and the fact that they are supplied together and interact means that: (a) they are intended to be used together, and (b) changes to one affect the other.

The problem is that, until now, it's been hard to prove A and B's interdependence with anything more substantial than gut feel. Now their interdependence can be demonstrated as a direct consequence of the Interface Principle.

Note that, unlike classes, namespaces don't need to be declared all at once, and what's "supplied together" depends on what parts of the namespace are visible.



//*** Example 6 (a) 


//---file a.h---


namespace N { class B; }// forward decl


namespace N { class A; }// forward decl


class N::A { public: void g(B); };


//---file b.h---


namespace N { class B { /*...*/ }; }


Clients of A include a.h, so for them A and B are supplied together and are interdependent. Clients of B include b.h, so for them A and B are not supplied together.

In summary, I'd like you to take away three thoughts from this miniseries.

  1. The Interface Principle: For a class X, all functions, including free functions, that both "mention" X and are "supplied with" X are logically part of X, because they form part of the interface of X.

  2. Therefore, both member and nonmember functions can be logically "part of " a class. A member function is still more strongly related to a class than is a nonmember, however.

  3. In the Interface Principle, a useful way to interpret "supplied with" is "appears in the same header and/or namespace." If the function appears in the same header as the class, it is "part of " the class in terms of dependencies. If the function appears in the same namespace as the class, it is "part of " the class in terms of object use and name lookup.

    I l@ve RuBoard Previous Section Next Section