masoud
masoud

Reputation: 56479

How to stop propagating declarations through hierarchical includes?

Whenever I make a .h header file, a question comes to my mind: "How to stop propagating declarations through hierarchical includes?" Assume there are these below files:

foo.h

#ifndef FOO_H
#define FOO_H

typedef int foo_t;

inline int foo() { return 1; }

class foo_c {};

#endif  /* FOO_H */

bar.h

#ifndef BAR_H
#define BAR_H

#include "Foo.h"

typedef foo_t bar_t;

inline int bar() { return foo(); }

class bar_c : public foo_c {};

#endif  /* BAR_H */

zoo.h

#ifndef ZOO_H
#define ZOO_H

#include "Bar.h"

typedef bar_t zoo_t;

inline int zoo() { return bar(); }

class zoo_c : public bar_c {};

#endif  /* ZOO_H */

In file zoo.h, we can access declared elements foo_c, foo_t, foo(), and every change to foo.h will re-compile zoo.h

I know we can move implementations to .cpp files, but how about the codes written in class definitions in .h files? How can we force the programmer to explicitly include foo.h in zoo.h if he needs it?

As an example in Qt, when I include and use <QQueue>, I have no access to QList where QQueue is inherited by QList and I have to include <QList> explicitly. (Also, I dont know how it is done, and effect of it on compile time)

Upvotes: 2

Views: 1471

Answers (5)

kfmfe04
kfmfe04

Reputation: 15327

I find it important to clearly separate forward-declarations vs definitions in my code: use forward-declarations as much as possible.

In general, if your class X does not need to know the sizeof class Y, all you need is a forward-declaration of Y - you do not need to include Y.hpp.

For example, if X does not subclass from Y and X does not contain any members of type Y, then you don't need to include Y.hpp. Forward-declaring class Y; is sufficient. Sometimes, to decouple my code better, I will hold a reference or pointer to Y rather than embed Y in class X - if this is feasible, again, all I need to do is forward-declare class Y;

Now, there is a comment about not being able to forward-declare when you use template classes. But there is a trick around this - instead of using typedef, subclass from the template instantiation that you want eg:

class Bars : public std::vector<Bar> { };

Now you can forward-declare class Bars;, where previously you could not forward declare std::vector<Bar>;

So, these are the steps I follow in all my C++ projects:

  1. separate my code into modules divided by namespaces
  2. create a fdecl.hpp file in each module that contains forward declarations for that module
  3. strongly prefer the use of #include <modulename/fdecl.hpp> over any #include <modulename/foo.hpp> (forward declarations over definitions)

In this way, the headers are loosely coupled and I get faster compile times as I modify code.

Upvotes: 3

Nicola Musatti
Nicola Musatti

Reputation: 18228

You can't have the cake and eat it too. Either you exploit inlining as much as you can or you limit visibility as much as you can. With classes you have to strike a balance between using derivation and/or direct data members, which require the corresponding class definition to be available, or indirect data members, i.e. pointers or references, which only require the class to be declared. Your approach favours inlining/direct inclusion, the opposite extreme would be:

foo.h

#ifndef FOO_H
#define FOO_H

typedef int foo_t;

int foo();

class foo_c {};

#endif  /* FOO_H */

bar.h

#ifndef BAR_H
#define BAR_H

typedef foo_t bar_t;

int bar();

class foo_c;

class bar_c {
  public:
    bar_c();
  private:
    foo_c * my_foo_c;
};

#endif  /* BAR_H */

zoo.h

#ifndef ZOO_H
#define ZOO_H

typedef bar_t zoo_t;

int zoo();

class zoo_c {
  public:
    zoo_c();
  private:
    bar_c * my_bar_c;
};

#endif  /* ZOO_H */

foo.c

#include "foo.h"

int foo() {
    return 1;
}

bar.c

#include "bar.h"
#include "foo.h"

int bar() {
    return foo();
}

bar_c::bar_c() : my_foo_c(new foo_c()) {}

zoo.c

#include "zoo.h"
#include "bar.h"

int zoo()
{
    return bar();
}

zoo_c::zoo_c() : my_bar_c(new bar_c()) {}

A sort of way in between would be to introduce an additional level of source files, which you might call .inl, move function implementations there and make them inline. In this way you could include these new files after the original headers and only where actually needed and get both limited visibility and maximum inlining. I don't think it is worth the effort, though.

Templates would complicate things even further because in general definitions must be available wherever a template needs to be instantiated. There are ways to control this, e.g. by forcing instantiations of the required specializations so as to avoid including definitions for every point of use, but again the added complication may not be worthwhile.

If you are worried about compilaton time usually it's much easier to rely on your compiler's header pre-compilation mechanism.

Upvotes: 1

Andriy Tylychko
Andriy Tylychko

Reputation: 16266

In C++ and C, "to stop propagating declarations" you need to remove them from public interface, period. Move them to implementation. Or to "less public" interface.

Compilation time is one of goals. Others are portability, maintenability. Also this is directly related with loose coupling.

The most popular C++ technique that can help with your class derivation is Pimpl idiom. Derive your implementation class, include corresponding header into implementation cpp and forward-declare implementation in your public interface. Your users will know nothing about base class and will know only the name of your implementation.

It's not possible to stop propagation if you'd like to use typedef's. But to provide better portability and maintenability you can use the same approach as Boost libraries use effectively: implementation-defined type (e.g. this one).

Each interface design is a tradeoff between extensibility, information hiding and simplicity (or effort). If you need to archive first two use more sophisticated approach. You can provide two public interfaces: one for usage and another one, much wider and lower-level, for extensibility.

Upvotes: 3

anatolyg
anatolyg

Reputation: 28269

Maybe you can use namespaces:

foo.h

namespace f {
    inline int foo();
}

bar.h

#include "foo.h"
inline int bar()
{
    using namespace f;
    return foo();
}

zoo.h

#include "bar.h"
inline int zoo()
{
    using namespace b;
    // Cannot use foo here: can only refer to it by the full name f::foo
    return bar();
}

This example looks contrived, but maybe only because the code is so short. If your application involves much more code, this trick may prove helpful.

Update:

The same principle can work with classes and other names. For example, with the Qt names:

qt_main.h

namespace some_obscure_name
{
    class QList {...};
    class QQueue: public QList {...}
    ...
}

qt_list.h

#include "qt_main.h"
using some_obscure_name::QList;

qt_queue.h

#include "qt_main.h"
using some_obscure_name::QQueue;

zoo.h:

#include "qt_queue.h"
...
QQueue myQueue; // OK
QList myList1; // Error - cannot use QList
some_obscure_name::QList myList2; // No error, but discouraged by Qt developers

Disclaimer: I have no experience with Qt; this example doesn't show what Qt developers actually did, it only shows what they could do.

Upvotes: 2

Vlad
Vlad

Reputation: 35594

I would rewrite the code in this way:

foo.h

#ifndef FOO_H
#define FOO_H

inline int foo();

#endif  /* FOO_H */

foo.cpp

#include "foo.h"

inline int foo()
{
    return 1;
}

bar.h

#ifndef BAR_H
#define BAR_H

inline int bar();

#endif  /* BAR_H */

bar.cpp

#include "bar.h"
#include "foo.h"

inline int bar()
{
    return foo();
}

zoo.h

#ifndef ZOO_H
#define ZOO_H

inline int zoo();

#endif  /* ZOO_H */

zoo.cpp

#include "zoo.h"
#include "bar.h"

inline int zoo()
{
    // cannot *incidentally* access foo() here, explicit #include "foo.h" needed
    return bar();
}

This way you expose in the header only your interface, and the implementation details remain in the .cpp file/.

Please be warned however that this strategy will fail if you work with templates: they have to be fully declared in the header (otherwise you are likely to get linker problems).

Upvotes: 2

Related Questions