In the previous lesson, we saw how we could split our code into multiple files, and then use the #include
directive to bring it back together before compilation.
We can think of the #include
directive as simply copying and pasting code from one file into another. That's quite crude, and it's not long before we encounter a problem. For example, lets imagine we have a file containing our Character class.
class Character {};
Lets create a Monster
class in another file.
#include "Character.cpp"
class Monster : public Character {};
This new file is referencing the Character
class, so we #include
the Character.cpp
file so the compiler knows what that is.
So far, so good. Unfortunately, if we now try to #include
our files into a third file (for example, our main.cpp
) our program will fail to compile.
#include "Character.cpp"
#include "Monster.cpp"
int main() {
Character PlayerCharacter;
Monster Goblin;
}
This will generate an error similar to below:
error: redefinition of ‘class Character’
To understand the error, lets imagine what our main.cpp
might look like as the preprocessor executes our #include
directives. First, it includes Character.cpp
and Monster.cpp
:
1// From #include "Character.cpp":
2class Character {};
3
4// From #include "Monster.cpp":
5#include "Character.cpp"
6class Monster : public Character {};
7
8int main() {
9 Character PlayerCharacter;
10 Monster Goblin;
11}
12
Note in particular the effect of #include Monster.cpp
- highlighted on line 5. Because Monster.cpp
also included Character.cpp
, we will now end up with the contents of Character.cpp
being placed into this file twice:
// From "Character.cpp":
class Character {};
// From "Monster.cpp":
class Character {};
class Monster : public Character {};
int main() {
Character PlayerCharacter;
Monster Goblin;
}
Hopefully now, the issue is clearer - our preprocessor directives are resulting in our main.cpp
file defining the Character
class twice, hence the compilation error message we see.
When following the above steps, you might have two possible ideas how to fix it. We could simply delete some #include
directives. After all, if main.cpp
can get the Character
class through including Monster.cpp
, it doesn't need to include Character.cpp
directly.
That is true, but it is generally not a good idea to rely on this. If one of our files has a dependency, it is much better to be explicit and clear about that, rather than relying on some side effects or implementation quirks that are going on in other files.
Otherwise, if someone opens our file in the future, it might be quite difficult to understand where Character
comes from, or indeed why our code works at all.
Worse, if someone makes a seemingly innocous change like reordering include directives, it might be quite difficult for them to understand why the code is no longer compiling.
Therefore, we should strive to be explicit in our code - particularly around dependencies. We should always directly express what our code needs to be in place for it to work.
So, lets see some better ways to prevent the preprocessor from duplicating our includes.
#ifndef
and #define
We can protect ourselves from this situation by using the conditional inclusion techniques we saw in the previous lesson.
This pattern is commonly called a header guard, because it is most commonly used with header files. Header files will be the topic of the next lesson.
By combining the #ifndef
(if not defined) directive with a #define
directive, we can insure that a block of code is only included once in any file. Typically, when used as a header guard, the entire contents of the file will be between the ifndef
and endif
directives:
#ifndef CHARACTER
#define CHARACTER
class Character {};
#endif
To understand how this solves the problem, lets walk through main.cpp
as seen by the preprocessor again, except this time, lets do it with our header guard for character.cpp
in place.
After evaluating all of our include
directives, the preprocessor will then be seeing this:
1// From #include "Character.cpp":
2#ifndef CHARACTER
3#define CHARACTER
4class Character {};
5#endif
6
7// From #include "Monster.cpp":
8#ifndef CHARACTER
9#define CHARACTER
10class Character {};
11#endif
12class Monster : public Character {};
13
14int main() {
15 Character PlayerCharacter;
16 Monster Goblin;
17}
18
Next, it's going to evaluate our conditional inclusion directives.
Line 2 is checking if a macro called CHARACTER
is not defined in our file. This is true - CHARACTER
is not defined, so lines 2 and 3 get included:
1// From #include "Character.cpp":
2#define CHARACTER
3class Character {};
4
5// From #include "Monster.cpp":
6#ifndef CHARACTER
7#define CHARACTER
8class Character {};
9#endif
10class Monster : public Character {};
11
12int main() {
13 Character PlayerCharacter;
14 Monster Goblin;
15}
16
Line 2 now defines a CHARACTER
macro for the rest of this file. So, when we reach line 6, CHARACTER
is defined this time. Therefore, lines 6-9 get excluded.
This leaves us with a totally valid main.cpp
file:
// From #include "Character.cpp":
class Character {};
// From #include "Monster.cpp":
class Monster : public Character {};
int main() {
Character PlayerCharacter;
Monster Goblin;
}
Our choice to call our macro CHARACTER
in the previous example has no connection to the fact that our class is called Character
. The preprocessor is not aware of C++ syntax.
It is conventional to define a meaningful name, but any unique name would have worked for us.
What problem is a header guard intended to solve?
How can we implement a header guard using conditional inclusion to prevent a file containing a Weapon
class from being included twice?
#pragma once
Header GuardThe combined use of the #ifndef
#define
and #endif
for this use case is so common that most compilers have made a single preprocessor directive that can replace the pattern.
This is the #pragma once
directive. If we had a file like the following example:
#ifndef CHARACTER
#define CHARACTER
class Character
{
// class definition here
}
#endif
We could instead simplify it to this:
#pragma once
class Character
{
// class definition here
}
#pragma once
is not part of the C++ standard, but it is included by every modern build tool.
We can use either the full conditional inclusion approach, or the #pragma once
shortcut. Different developers (and companies) will have different preferences on what to use.
"Do not use #pragma once
[...] All header files should have #define
guards to prevent multiple inclusion." - Google Style Guide
"All headers should protect against multiple includes with the #pragma once
directive. Note that all compilers we use support #pragma once
." - Unreal Engine Style Guide
We will use the #pragma once
approach in all our examples going forward.
Aside from the #ifndef
technique, what is another way to implement a header guard?
Our example above only applied header guards to the file that was specifically causing the problem - Character.cpp
But, we don't know how our code files are going to be included in the future, so it is recommended to include header guards proactively.
Even if Monster.cpp
is not being included multiple times right now, it may be in the future. It's generally going to be much easier to protect against these issues ahead of time than trying to fix them as they crop up.
#pragma once
#include "Character.cpp"
class Monster : public Character {};
Up next, we'll introduce the concept of Header Files which will show us a more industry-standard way to organise our projects.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way