*****************************************************
** 5) Why that is the "right way" to include **
*****************************************************
Note: in this section I refer to the "right way" method outlined above as "mine". While I did come up with it on my own (after struggling through the mucky muck for a while) -- I can't say I was the first person who ever thought of it, so it isn't really "mine". But for purposes of this article, I call it "mine" for simplicity.
You: "So-and-so says that #including in a header is dangerous, but you say it's not! Why is your way so much better than what so-and-so says?"
So-and-so is partially right, but is explaining it wrong. Frivilous and careless #includes
can lead to trouble. And one way to avoid those troubles
is to never #include inside a header file. So yeah, so-and-so's heart is in the right place. But ultimately, using so-and-so's approach is going to give yourself TONS of additional work and headaches.
The concept I'm illustrating is very OO, and enhances encapsulation. The general idea is that it makes "myclass.h" fully self-contained and doesn't require any other area of the program (other than MyClass's implementation/source file) to know how MyClass works internally. If some other class needs to use MyClass, it can just #include "myclass.h" and be done with it!
The alternative (so-and-so's method), would require you to #include all of MyClass's dependencies
before #including "myclass.h", since myclass.h can't include its dependencies itself. This is headache city, because now using a class isn't so straightforward.
Here is an example of why my method is good:
1 2 3 4 5 6 7
|
//example.cpp
// I want to use MyClass
#include "myclass.h" // will always work, no matter what MyClass looks like.
// You're done
// (provided myclass.h follows my outline above and does
// not make unnecessary #includes)
|
Here is an example of why so-and-so's method is bad:
1 2 3 4 5
|
//example.cpp
// I want to use MyClass
#include "myclass.h"
// ERROR 'Parent' undefined
|
so-and-so: "Hrm... okay...."
1 2 3
|
#include "parent.h"
#include "myclass.h"
// ERROR 'std::vector' undefined
|
1 2 3 4
|
#include "parent.h"
#include <vector>
#include "myclass.h"
// ERROR 'Support' undefined
|
so-and-so: "WTF? MyClass doesn't even
use Support! But alright..."
1 2 3 4 5
|
#include "parent.h"
#include <vector>
#include "support.h"
#include "myclass.h"
// ERROR 'Support' undefined
|
so-and-so: "Give me a break! I'm including it! What else do you want!"
Believe it or not, the above
does happen. Little did poor so-and-so know, but "parent.h" uses Support, and therefore you must #include "support.h"
before "parent.h".
And what happens if support.h needs something else? What if
that something else needs something else? We're already up to 4 #includes just to use a single class! With so-and-so's method, not only do you have to remember which includes are needed for each class, but also
the order in which you need to #include them. This becomes a
huge nightmare
very quickly.
And what happens if you want to tweak MyClass? Let's say you decide that it would be better to use std::list instead of std::vector. With so-and-so's method, you now have to go back and change
every single file that #includes "myclass.h" and change it to include <list> instead of <vector> (which might be dozens of files depending on the size of the project and how often MyClass is used), whereas with my method you only have to change "myclass.h", and maybe "myclass.cpp".
The "right way" I illustrated above is all about encapsulation. Files that want to use MyClass don't need to be aware of what MyClass uses in order for it to work, and don't need to #include any MyClass dependencies. All you need to do to get MyClass to work is #include "myclass.h". Period. The header file is set up to be completely self contained. It's all very OO friendly, very easy to use, and very easy to maintain.
*****************************************************
** 6) Circular Dependencies **
*****************************************************
A circular dependency is when two (or more) classes depend on each other. For example, class A depends on class B, and class B depends on class A.
If you stick to "the right way" and forward declare when you can instead of #including needlessly, this usually isn't a problem. As long as the circle is broken with a forward declaration at some point, you're fine.
Here's the perfect example of why you should only #include what is necessary:
1 2 3 4
|
// a.h -- assume it's guarded
#include "b.h"
class A { B* b; };
|
1 2 3 4
|
// b.h -- assume it's guarded
#include "a.h"
class B { A* a };
|
An initial glance might see nothing wrong with this. B is a dependency of A, so you include it, and A is a dependency of B, so you include it. So what's wrong with this?
This is a circular inclusion (also called an infinite inclusion) and is the result of one or more includes that shouldn't be there. Say for example you compile "a.cpp":
1 2
|
// a.cpp
#include "a.h"
|
The compiler will do the following:
1 2 3 4 5 6 7 8 9 10 11 12
|
#include "a.h"
// start compiling a.h
#include "b.h"
// start compiling b.h
#include "a.h"
// compilation of a.h skipped because it's guarded
// resume compiling b.h
class B { A* a }; // <--- ERROR, A is undeclared
|
Even though you're #including "a.h", the compiler is not seeing the A class until after the B class gets compiled. This is because of the circular inclusion problem. This is why you should
always forward declare when you're only using a pointer or reference. Here, "a.h" should not be #including b.h, but instead should just be forward declaring B. Likwise, b.h should be forward declaring A. If you make those changes, the problem is solved.
The circular inclusion problem may persist if both dependencies are #include dependencies (ie: they can't be forward declared). Here's an example:
1 2 3 4 5 6 7 8
|
// a.h (guarded)
#include "b.h"
class A
{
B b; // B is an object, can't be forward declared
};
|
1 2 3 4 5 6 7 8
|
// b.h (guarded)
#include "a.h"
class B
{
A a; // A is an object, can't be forward declared
};
|
You may note, however, that this situation is
conceptually impossible. There is a fundamental design flaw. If A has a B object, and B has an A object, then A contains a B, which contains another A, which contains another B, which contains another A, which contains another B, etc, etc. You have an infinite recursion problem, and either class is simply impossible to instantiate. The solution is to have one or both classes contain a
pointer or reference to the other, rather than a full object. Then you can forward declare, and then you can get around the circular inclusion problem.