GotW #7c Solution: Minimizing Compile-Time Dependencies, Part 3

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.

26 thoughts on “GotW #7c Solution: Minimizing Compile-Time Dependencies, Part 3

  1. I think you forgot to remove B from the list of X’s base classes, unless I’m badly misunderstanding something.

  2. At time of writing, the “after” still has the inheritance
    [CODE]class X : public A, private B {[/CODE]
    and not the composition-only
    [CODE]class X : public A {[/CODE]

  3. You missed removing “private B” from X’s super-classes.
    Also, shouldn’t B be forward-declared? There are methods in X which make use of it.

  4. Umm, don’t you want to remove the ‘private B’ from the class X declaration line and add a ‘class B;’ declaration so the compiler won’t complain about the second member function declaration?

  5. Umm, looks like you forgot to actually remove the inheritance from B. Also needs to have a forward decl for B for the second and fourth member functions.

  6. Hi,

    I maybe mistaken but in the final version of the header there is still a private inheritance from B. Wasn’t it the whole point to remove it ? :)

    Furthermore, i’m kinda confused. I agree that using composition you can get rid of the

    #include "b.h"

    , but B is still reference in the public part of the class X:

    B  f( int, char* );
    C& g( B );
    

    Will it still work if we remove the

    #include "b.h"

    ?

    Thx

    Alex

  7. Your new improved x.h still says “, private B” and fails to say “class B;”

  8. Typo in the final solution as X still inherits from B. There also needs to be a forward declaration of B as it is both a return value and parameter for X’s functions.

  9. The last code example has some errors. First of all, in the code X still (privately) inherits from B. Since it is now part of the impl, you don’t need to inherit anymore. Furthermore f returns B by value and g takes B by value, which means that you can’t remove the include for B. And lastly either you have to change unique_ptr to std::unique_ptr or “add using namespace std”.

  10. You forgot to excise “, private B” from the final version… and forward declare “class B”.

  11. Surely there’s a typo here? You’re still writing

    class X : public A, private B {

    but now B is nowhere defined, and is supposedly hidden entirely in the pimpl. Shouldn’t you be deleting

    , private B

    ?

  12. You forgot to remove class B from inheritance list on the last code example.

  13. Oops, looks like you forgot to remove the inheritance from the corrected code :)

    class X : public A, <>

  14. @all: Yup, thanks for the cut-and-paste visual; I’m not sure how the old code resurfaced, but now it’s fixed. Acknowledgements to the first three who reported specific aspects: juanchopanza, anicolaescu, Bert Rodiers. Thanks, corrected.

  15. The inheritance is fixed but you added the

    #include "b.h"

    again :D

    But i think this header is still necessary since the class B is still reference by value in the public interface of the class X (thus B can’t be forward declared).

  16. Second attempt of copy-and-paste still failed; you can remove #include “b.h”, and you must forward-declare B.

  17. I think you forgot to change the inclusion of b.h in the final version to a forward declaration of B since there are no users left.

  18. Why are so many people repeating the same things in comments? You would think once someone has pointed out that the #include has not been removed that it would become publicly known and not need further pointing out.

  19. @Lachlan: It was because the comments were held for moderation, so they didn’t see each other’s comments. Then I approved them all, not wanting to quash anyone and crediting the first to point out the typos.

  20. @Alexandre Chassany

    Refer to the solution for #7a, including Sebastian Redl’s comment, which discussed eliminating the E.h include directive because E was used only as an argument and a return type. B is now used in exactly the same ways.

Comments are closed.