It's a pretty interesting question, really. Managing dependencies is important for big projects because the build times ramp up can make even the simplest change daunting... and when it happens people will try to hack it to avoid the rebuild-of-death (tm).
Unfortunately, it does not work.
The Standard explicitly says that classes definitions appearing in different translation units (roughly, files) should obey the One Definition Rule (see ยง 3.2 One definition rule [basic.def.odr]).
Why ?
The problem is a matter of impedance, in a way. The definition of a class contains information on the class ABI (Application Binary Interface), most notably, how such a class is layed out in memory. If you have different layouts of the same class in various translation units, then when putting it altogether, it won't work. It's as if one TU was speaking German and the other Korean. They might be attempting to say the same thing, they just won't understand each other.
So ?
There are several ways to manage dependencies. The main idea is that you should struggle, as much as possible, to provide "light" headers:
- include as few things as possible. You can forward declare: types that are shown as arguments or return of functions declaration, types that are passed by reference or pointer but otherwise unused.
- hide implementation details
Hum... What does it mean :x ?
Let's pick a simple example, shall we ?
#include "project/a.hpp" // defines class A
#include "project/b.hpp" // defines class B
#include "project/c.hpp" // defines class C
#include "project/d.hpp" // defines class D
#include "project/e.hpp" // defines class E
namespace project {
class MyClass {
public:
explicit MyClass(D const& d): _a(d.a()), _b(d.b()), _c(d.c()) {}
MyClass(A a, B& b, C* c): _a(a), _b(b), _c(c) {}
E e() const;
private:
A _a;
B& _b;
C* _c;
}; // class MyClass
} // namespace project
This header pulls in 5 other headers, but how many are actually necessary ?
a.hpp
is necessary, because _a
of type A
is an attribute of the class
b.hpp
is not necessary, we only have a reference to B
c.hpp
is not necessary, we only have a pointer to C
d.hpp
is necessary, we call methods on D
e.hpp
is not necessary, it only appears as a return
OK, let's clean this up!
#include "project/a.hpp" // defines class A
#include "project/d.hpp" // defines class D
namespace project { class B; }
namespace project { class C; }
namespace project { class E; }
namespace project {
class MyClass {
public:
explicit MyClass(D const& d): _a(d.a()), _b(d.b()), _c(d.c()) {}
MyClass(A a, B& b, C* c): _a(a), _b(b), _c(c) {}
E e() const;
private:
A _a;
B& _b;
C* _c;
}; // class MyClass
} // namespace project
Can we do better ?
Well, first we can see that we call methods on D
only in the constructor of the class, if we move the definition of D
out of the header, and put it in a .cpp
file, then we won't need to include d.hpp
any longer!
// no need to illustrate right now ;)
But... what of A
?
It is possible to "cheat", by remarking that merely holding a pointer does not requires a full definition. This is known as the Pointer To Implementation idiom (pimpl for short). It trades off run time for lighter dependencies, and adds some complexity to the class. Here is a demo:
#include <memory> // don't really worry about std headers,
// they are pulled in at one time or another anyway
namespace project { class A; }
namespace project { class B; }
namespace project { class C; }
namespace project { class D; }
namespace project { class E; }
namespace project {
class MyClass {
public:
explicit MyClass(D const& d);
MyClass(A a, B& b, C* c);
~MyClass(); // required to be in the source file now
// because for deleting Impl,
// the std::unique_ptr needs its definition
E e() const;
private:
struct Impl;
std::unique_ptr<Impl> _impl;
}; // class MyClass
} // namespace project
And the corresponding source file, since that were the interesting things occur:
#include "project/myClass.hpp" // good practice to have the header included first
// as it asserts the header is free-standing
#include "project/a.hpp"
#include "project/b.hpp"
#include "project/c.hpp"
#include "project/d.hpp"
#include "project/e.hpp"
struct MyClass::Impl {
Impl(A a, B& b, C* c): _a(a), _b(b), _c(c) {}
A _a;
B& _b;
C* _c;
};
MyClass::MyClass(D const& d): _impl(new Impl(d.a(), d.b(), d.c())) {}
MyClass::MyClass(A a, B& b, C* c): _impl(new Impl(a, b, c)) {}
MyClass::~MyClass() {} // nothing to do here, it'll be automatic
E MyClass::e() { /* ... */ }
Okay, so that was the low and gritty. Further reading:
- The Law of Demeter: avoid having to call multiple methods in sequences (
a.b().c().d()
), it means you have leaky abstraction, and forces you the include the whole world to do anything. Instead, you should be calling a.bcd()
which hides the details from you.
- Separate your code into modules, and provide a clear-well defined interface to each module, normally, you should have far more code within the module than on its surface (ie exposed headers).
There are many ways to encapsulate and hide information, your quest just begins!