Now the unnecessary headers have been removed, and avoidable dependencies on the internals of the class have been eliminated. Is there any further decoupling that can be done? The answer takes us back to basic principles of solid class design.
Problem
JG Question
1. What is the tightest coupling you can express in C++? And what’s the second-tightest?
Guru Question
2. The Incredible Shrinking Header has now been greatly trimmed, but there may still be ways to reduce the dependencies further. What further #includes could be removed if we made further changes to X, and how?
This time, you may make any changes at all to X as long as they don’t change its public interface, so that existing code that uses X is unaffected. Again, note that the comments are important.
// x.h: after converting to use a Pimpl to hide implementation details
//
#include <iosfwd>
#include <memory>
#include "a.h" // class A (has virtual functions)
#include "b.h" // class B (has no virtual functions)
class C;
class E;
class X : public A, private B {
public:
X( const C& );
B f( int, char* );
C f( int, C );
C& g( B );
E h( E );
virtual std::ostream& print( std::ostream& ) const;
private:
struct impl;
std::unique_ptr<impl> pimpl; // ptr to a forward-declared class
};
std::ostream& operator<<( std::ostream& os, const X& x ) {
return x.print(os);
}
Solution
1. What is the tightest coupling you can express in C++? And what’s the second-tightest?
Friendship and inheritance, respectively.
A friend of a class has access to everything in that class, including all of its private data and functions, and so the code in a friend depends on every detail of the type. Now that’s a close friend!
A class derived from a class Base has access to public and protected members in Base, and depends on the size and layout of Base because it contains a Base subobject. Further, the inheritance relationship means that a derived type is at least by default substitutable for its Base; whether the inheritance is public or nonpublic only changes what other code can see and make use of the substitutability. That’s pretty tight coupling, second only to friendship.
2. What further #includes could be removed if we made further changes to X, and how?
Many programmers still seem to march to the “It isn’t OO unless you inherit!” battle hymn, by which I mean that they use inheritance more than necessary. I’ll save the whole lecture for another time, but my bottom line is simply that inheritance (including but not limited to IS-A) is a much stronger relationship than HAS-A or USES-A. When it comes to managing dependencies, therefore, you should always prefer composition/membership over inheritance wherever possible. To paraphrase Einstein: ‘Use as strong a relationship as necessary, but no stronger.’
In this code, X is derived publicly from A and privately from B. Recall that public inheritance should always model IS-A and satisfy the Liskov Substitutability Principle (LSP). In this case X IS-A A and there’s naught wrong with it, so we’ll leave that as it is.
But did you notice the curious thing about B‘s virtual functions?
“What?” you might say. “B has no virtual functions.”
Right. That is the curious thing.
B is a private base class of X. Normally, the only reason you would choose private inheritance over composition/membership is to gain access to protected members—which most of the time means “to override a virtual function.” (There are a few other rare and obscure reasons to inherit, but they’re, well, rare and obscure.) Otherwise you wouldn’t choose inheritance, because it’s almost the tightest coupling you can express in C++, second only to friendship.
We are given that B has no virtual functions, so there’s probably no reason to prefer the stronger relationship of inheritance—unless X needs access to some protected function or data in B, of course, but for now I’ll assume that this is not the case. So, instead of having a base subobject of type B, X probably ought to have simply a member object of type B. Therefore, the way to further simplify the header is:
(a) Remove unnecessary inheritance from class B.
#include "b.h" // class B (has no virtual functions)
Because the B member object should be private (it is, after all, an implementation detail), and in order to get rid of the b.h header entirely, this member should live in X‘s hidden pimpl portion.
Guideline: Never inherit when composition is sufficient.
This leaves us with header code that’s vastly simplified from where we started in GotW #7a:
// x.h: after removing unnecessary inheritance
//
#include <iosfwd>
#include <memory>
#include "a.h" // class A (has virtual functions)
class B;
class C;
class E;
class X : public A {
public:
X( const C& );
B f( int, char* );
C f( int, C );
C& g( B );
E h( E );
virtual std::ostream& print( std::ostream& ) const;
private:
struct impl;
std::unique_ptr<impl> pimpl; // this now quietly includes a B
};
std::ostream& operator<<( std::ostream& os, const X& x ) {
return x.print(os);
}
After three passes of progressively greater simplification, the final result is that x.h is still using other class names all over the place, but clients of X need only pay for three #includes: a.h, memory, and iosfwd. What an improvement over the original!
Acknowledgments
Thanks in particular to the following for their feedback to improve this article: juanchopanza, anicolaescu, Bert Rodiers.
