GotW #7

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book Exceptional C++ (Addison-Wesley, 2000) for the most current solutions to GotW issues #1-30. 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.

Compile-Time Dependencies
Difficulty: 7 / 10

Most programmers #include many more headers than necessary. Do you? To find out, consider this issue's problem.

Problem

[Warning: It's tougher than it looks. The comments are important.]

Most programmers #include much more than necessary. This can seriously degrade build times, especially when a popular header file includes too many other headers.

In the following header file, what #include directives could be immediately removed without ill effect? Second, what further #includes could be removed given suitable changes, and how? (You may not change the public interfaces of classes X and Y; that is, any changes you make to this header must not affect current client code).

    // gotw007.h (implementation file is gotw007.cpp)
    //
    #include "a.h"  // class A
    #include "b.h"  // class B
    #include "c.h"  // class C
    #include "d.h"  // class D
                    // (note: only A and C have virtual functions)
    #include <iostream>
    #include <ostream>
    #include <sstream>
    #include <list>
    #include <string>

    class X : public A {
    public:
             X        ( const C& );
        D    Function1( int, char* );
        D    Function1( int, C );
        B&   Function2( B );
        void Function3( std::wostringstream& );
        std::ostream& print( std::ostream& ) const;
    private:
        std::string  name_;
        std::list<C> clist_;
        D            d_;
    };
    std::ostream& operator<<( std::ostream& os, const X& x )
        { return x.print(os); }

    class Y : private B {
    public:
        C  Function4( A );
    private:
        std::list<std::wostringstream*> alist_;
    };

Solution

First, consider the #includes that can be immediately removed. Here is the original header again as a reminder:

    // gotw007.h (implementation file is gotw007.cpp)
    //
    #include "a.h"  // class A
    #include "b.h"  // class B
    #include "c.h"  // class C
    #include "d.h"  // class D
                    // (note: only A and C have virtual functions)
    #include <iostream>
    #include <ostream>
    #include <sstream>
    #include <list>
    #include <string>

    class X : public A {
    public:
             X        ( const C& );
        D    Function1( int, char* );
        D    Function1( int, C );
        B&   Function2( B );
        void Function3( std::wostringstream& );
        std::ostream& print( std::ostream& ) const;
    private:
        std::string  name_;
        std::list<C> clist_;
        D            d_;
    };
    std::ostream& operator<<( std::ostream& os, const X& x )
        { return x.print(os); }

    class Y : private B {
    public:
        C  Function4( A );
    private:
        std::list<std::wostringstream*> alist_;
    };

1. We can immediately remove:

- iostream, because although streams are being used nothing in iostream specifically is being used

- ostream and sstream, because parameter and return types need only be forward-declared and hence only the iosfwd header is needed (note that there are no comparable 'stringfwd' or 'listfwd' standard headers; iosfwd was introduced for backwards compatibility so as not to break code written for the old non-templated versions of the streams subsystem)

We cannot immediately remove:

- a.h, because A is a base class of X

- b.h, because B is a base class of Y

- c.h, because many current compilers require list<C> be able to see the definition of C (this should be fixed on future versions of those compilers)

- d.h, list and string, because X needs to know the size of D and string, and both X and Y need to know the size of list

Second, consider the #includes that can be removed by hiding the implementation details of X and Y:

2. We can remove d.h, list and string by letting X and Y use pimpl_'s (that is, the private parts are replaced by pointers to a forward- declared type of implementation object), because then X and Y do not need to know the sizes of D or list or string. This also lets us get rid of c.h, since besides in X::clist_ C objects only appear as parameters and return values.

Important Note: The inlined free operator<< may still be inlined and use its ostream parameter, even though ostream has not been defined! This is because you only need the definition if you are going to call member functions, not if you are only going to accept an object and do nothing with it except use it as parameters to other function calls.

Finally, consider what we can fix by making other small changes:

3. We can remove b.h by noticing that it is a private base class of Y but that B has no virtual functions. The only major reason one would choose private inheritance over composition/containment is to override virtual functions. Hence, instead of inheriting from B, Y should have a member of type B. To remove the b.h header, this member should be in Y's hidden pimpl_ portion.

Guideline: Prefer using pimpl_'s (pointers to implementations) to insulate client code from implementation details.

Excerpted from the GotW coding standards:

- encapsulation and insulation:

- avoid showing private members of a class in its declaration:

- use an opaque pointer declared as "struct XxxxImpl* pimpl_" to store private members (incl. both state variables and member functions), e.g., class Map { private: struct MapImpl* pimpl_; }; (Lakos96: 398-405; Meyers92: 111-116; Murray93: 72-74)

4. We still can't do anything about a.h since A is used as a public base class, and the IS-A relationship is probably needed and used by client code since A has virtual functions. However, we could at least mitigate this by noticing that X and Y are fundamentally unrelated, and splitting the definitions of classes X and Y into two separate headers (providing the current header as a stub which includes both x.h and y.h, so as not to break existing code). This way, at least y.h does not need to include a.h since it only uses A as a function parameter type, which does not require a definition.

Putting it all together, we get much cleaner headers:

    //---------------------------------------------------------------
    // new file x.h: only TWO includes!
    //
    #include "a.h"  // class A
    #include <iosfwd>

    class C;
    class D;

    class X : public A {
    public:
             X        ( const C& );
        D    Function1( int, char* );
        D    Function1( int, C );
        B&   Function2( B );
        void Function3( std::wostringstream& );
        std::ostream& print( std::ostream& ) const;
    private:
        class XImpl* pimpl_;
    };

    inline std::ostream& operator<<( std::ostream& os, const X& x )
        { return x.print(os); }
        // NOTE: this does NOT require ostream's definition!


    //---------------------------------------------------------------
    // new file y.h: ZERO includes!
    //
    class A;
    class C;

    class Y {
    public:
        C  Function4( A );
    private:
        class YImpl* pimpl_;
    };


    //---------------------------------------------------------------
    // gotw007.h is now just a compatibility stub with two lines, and
    // pulls in only TWO extra secondary includes (through x.h)
    //
    #include "x.h"
    #include "y.h"


    //---------------------------------------------------------------
    // new structures in gotw007.cpp... note that the impl objects
    // will be new'd by the X/Y ctors and delete'd by the X/Y dtors
    // and X/Y member functions will access the data through their
    // pimpl_ pointers
    //
    struct XImpl    // yes, this can be called "struct" even
    {               // though the forward-decl says "class"
        std::string  name_;
        std::list<C> clist_;
        D            d_;
    }

    struct YImpl
    {
        std::list<std::wostringstream*> alist_;
        B b_;
    }

Bottom Line: Clients of X need only pay for the #includes of a.h and iosfwd. Current clients of Y need only pay for the #includes of a.h and iosfwd, and if they are later updated to use y.h instead of gotw007.h they need pay for no secondary #includes at all. What an improvement over the original!

Copyright © 2009 Herb Sutter