The #include Directive

Discover how the #include directive helps us include library features and split our project across multiple files.
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a character on a desert planet
Ryan McCombe
Ryan McCombe
Edited

In the previous lesson, we introduced the preprocessor, and how we can use it to conditionally remove parts of our code before we send it off for compilation.

The second aspect of the preprocessor that we're looking at essentially does the opposite. It gives us a way to add additional code to our files.

This can be done using the #include directive. You may have noticed we've already been using an include directive this whole time:

#include <iostream>

We can finally explain what that line is doing!

Libraries and the Standard Library

When we build our project, the preprocessor will see this #include directive, and it will replace it with a load of code that is stored in a different location.

This is useful for two reasons:

  • When our project gets bigger, it allows us to arrange our code across multiple files, keeping everything organized
  • It gives us access to libraries - code and features that other developers and organizations have written

Libraries help us build features much more quickly. It means we don’t have to reinvent the wheel every time.

C++ comes with a library of code by default, which is called the standard library.

The #include <iostream> directive is adding standard library code into our file. Specifically, it is inserting code that allows us to work with input and output streams. It’s what has enabled us to use cout in all our examples thus far.

As a side effect, this directive has also been adding the standard library’s string class to our files, which we’ve been using in quite a few examples.

The string class is also available by itself. If we need string, but not cout, we could use this directive instead:

#include <string>

In the next courses, we’ll see many more examples of using the standard library. We’ll also be importing code from third-party libraries.

These will give us the ability to create windows, handle keyboard and mouse input, communicate over the internet, and more.

Using #include with our files

The #include directive also allows us to insert the content from other files in our project.

This gives us the ability to separate blocks of code - typically a class - to their own dedicated file, and then #include them into other files that need them.

This effectively allows us to extend the idea of encapsulation to cover our project organization, too.

For example, all of our character-related code can go into the Character class, and then that class can go into the Character.cpp file.

The following shows an example of this. Note we will explain the difference between the <> and "" syntax in the next section:

// main.cpp
#include <iostream>
#include "Character.cpp"

int main() {
  Character Player;
}
// Character.cpp
class Character {
  // ...
};

Once the preprocessor encounters #include "Character.cpp" in our main.cpp, it will jump into action and replace that directive with the contains of our Character.cpp file.

Because of this, the code that our compiler receives will effectively look like this:

// main.cpp
#include <iostream>
class Character {
  // ...
};

int main() {
  Character Player;
}

This file now looks very similar to what we've seen in the past. Now, however, instead of having to work with a main.cpp that constantly gets bigger, we're able to organize our project into multiple files, each of them much easier to work with.

<> vs " when using #include

In the previous example, we saw two ways of using the #include directive. One used angle brackets: < and >, whilst the other used double quotes: "

#include <iostream>
#include "Character.cpp"

The different format here broadly relates to where the preprocessor should look for the requested file.

The specific details vary from compiler to compiler, but in most situations:

  • <> will search within the "include directories" we have set for our project (we cover how to set these in the next section)
  • "" will search relative to the current file, where we’re using the #include directive. If it can’t find what we’re looking for, it will then fall back to searching include directories, as if we had used <>

This behavior gives us some flexibility - we’d often get the same result whether we use <> or "". A common convention is to:

  • use the "" format when we’re including files that are within our project
  • use the <> format when we’re including external, "library" files

Include Directories and Traversal

Where exactly to look for those files that are located elsewhere is defined within our project settings. They are typically called include directories, and how exactly we set them varies from IDE to IDE.

In Visual Studio, this is under Project Settings > C/C++ > Include Directories

When using the #include directive, the file we’re looking for may not be in the same directory as our current file, and it may not be in any of our include directories specifically.

For these scenarios, we may need to use some traversal techniques to specify exactly where the file is. For example, it might be in a subdirectory or a parent directory.

We can #include a file in a subdirectory using the / syntax. In the following example, we’re trying to include Math.cpp, which is in a subdirectory called Helpers:

#include "Helpers/Math.cpp"

We can navigate up the directory tree using ../ syntax. In the following example, we’re trying to include Physics.cpp in the parent directory, and Math.cpp in the grandparent directory:

// Traverse up one level
#include "../Physics.cpp"

// Traverse up two levels
#include "../../Math.cpp"

Finally, we can combine both techniques. In the following example, we’re trying to navigate into Helpers within the parent directory, to include a Math.cpp file stored there:

#include "../Helpers/Math.cpp"

In general, our #include directives should not get particularly complex. If we find ourselves frequently needing to perform elaborate traversal, it’s worth trying to eliminate it.

This can be done by reorganizing our directories or adding new include directories in our project settings.

Test your Knowledge

The #include Directive

How can we use the preprocessor to include the content from Utilities.cpp, which is in the same directory as the file we’re working on?

How can we use the preprocessor to include the contents of a file called Utilities.cpp which is in the parent directory?

Redefinition Errors when using #include

Given that the #include directive is simply copying and pasting code from one file into another, it can introduce errors in our project.

For example, let's imagine we have a file containing our Character class.

class Character {};

Let's 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. Individually, all of our files are perfectly valid. However, if we now try to #include them in our main file to use our new classes, our program will fail to compile.

#include "Character.cpp"
#include "Monster.cpp"

int main() {
  Character Player;
  Monster Enemy;
}

This will generate an error similar to below:

error: redefinition of ‘class Character

To understand the error, let's imagine what our main.cpp might look like as the preprocessor executes our #include directives. First, it includes Character.cpp and Monster.cpp:

// From #include "Character.cpp":
class Character {};

// From #include "Monster.cpp":
#include "Character.cpp"
class Monster : public Character {};

int main() {
  Character PlayerCharacter;
  Monster Goblin;
}

Note in particular the effect of #include Monster.cpp.

Because Monster.cpp also included Character.cpp, we will now end up with the contents of Character.cpp being placed into our file twice:

// From "Character.cpp":
class Character {};

// From "Monster.cpp":
class Character {};
class Monster : public Character {};

int main() {
  Character PlayerCharacter;
  Monster Goblin;
}

Hopefully, the "Redefinition of Character" class error makes sense in this context. Our #include directives are resulting in our main.cpp file defining the Character class twice.

Indirect Dependencies

When following the above steps, you might have identified a simple fix: within our main.cpp file, delete the #include "Character.cpp" line.

After all, if main.cpp is going to get the contents of that file indirectly anyway, through the #incude Monster.cpp directive.

That is true, and it would fix the problem, 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 innocuous change in one file, it could generate cryptic error messages in files they haven’t changed. It likely won’t be obvious to them why a file they haven’t changed has suddenly stopped working.

Therefore, we should strive to be explicit in our code - particularly around dependencies. We should directly express what our code needs to be in place for it to work.

The next sections introduce some better ways to fix this problem

Header Guards using #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 ensure 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, let's walk through main.cpp as seen by the preprocessor again, except this time, let's do it with our header guard for Character.cpp in place.

After evaluating all of our #include directives, the preprocessor will see 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}

Next, it's going to evaluate our conditional inclusion directives, from top to bottom.

Line 2 is checking if a macro called CHARACTER is not defined in our file. This is true - CHARACTER is not defined, so everything from the #ifndef to #endif gets included. That is the following two lines:

#define CHARACTER
class Character {};

Our main.cpp file now looks like this:

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}

A CHARACTER macro is now defined for the rest of this file. So, when we reach the next #ifndef CHARACTER directive, CHARACTER is defined this time. Therefore, lines 6-9 are excluded.

This leaves us with a valid main.cpp file that gets sent to the compiler:

1// From #include "Character.cpp":
2class Character {};
3
4// From #include "Monster.cpp":
5class Monster : public Character {};
6
7int main() {
8  Character PlayerCharacter;
9  Monster Goblin;
10}

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.

Test your Knowledge

Header Guards

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?

The #pragma once Header Guard

The 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.

All headers should protect against multiple includes with the #pragma once directive. Note that all compilers we use support #pragma once.

We will use the #pragma once approach in all our examples going forward.

Test your Knowledge

Shortened Header Guards

How could we rewrite the following file, whilst ensuring the header guard is maintained?

#ifndef WEAPON
#define WEAPON
class Weapon {};
#endif

Always use Header Guards

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 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 standardized way to organize our projects.

A Note on C++20 Modules

When we use the #include directive, we can think of it as copying and pasting the contents of a file into another file. This is quite a crude operation, and it has some issues we need to manage.

The C++20 standard, published in December 2020, introduced a new feature to the language called modules. They use the C++ import syntax, rather than the preprocessor's #include.

// Before
#include "Character.cpp"

// After
import Character;

Modules are a bit smarter than the basic #include directive, and they will supersede it eventually.

Modules will supersede #include directives eventually, and we cover them in more detail in the advanced course. However, for now, it's more important to understand how the #include directive works.

Things move slowly in the C++ world. As of 2024, few compilers fully support modules yet and, even once they do, #include will still be more common for many years to come.

Summary

In this lesson, we explored the #include directive, explaining its role in adding code from different files and libraries to a project. We also addressed common issues such as redefinition errors and the use of header guards.

The key things we learned are:

  • The #include directive allows the addition of code from libraries and other project files, aiding in project organization and feature expansion.
  • There is a small distinction between <> and " in #include directives, with <> typically used for libraries and "" for project-specific files.
  • We covered techniques for handling file paths in #include directives, including traversal through directories and setting #include directories in the IDE.
  • We learned how redefinition errors can be caused by multiple inclusions of the same file.
  • We revisited the conditional inclusion directives (#ifndef, #define, #endif) in the context of header guards, and the shorter #pragma once for preventing multiple inclusions of files.

Preview

In the next lesson, we will delve into the concepts of header files and the linker. This lesson will explain how to effectively use header files to organize code and interface declarations, and how the linker plays a crucial role in combining compiled code into a functioning program.

We will cover:

  • Understanding the purpose and structure of header files.
  • Best practices for organizing code using header files and separating implementation.
  • The role of the linker in combining object files to create an executable.
  • Techniques for resolving common linker errors and issues.
  • The relationship between header files, source files, and the linking process.

Was this lesson useful?

Edit History

  • Refreshed Content

  • First Published

Ryan McCombe
Ryan McCombe
Edited
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
The Preprocessor and the Linker
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

Header Files

Explore how header files and linkers streamline C++ programming, learning to organize and link our code effectively
3D art showing a character in a bar
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved