C++ Development Tutorial 3: Compile Multiple Files (2) — Header Files

Domi Yan
5 min readMay 3, 2020

In the last tutorial, we talked about the basic compilations steps (preprocessing, compilation, linking) in the C++ and two important concepts “forward declaration” and “one definition rule”. If you haven’t read it, I strongly recommend reading it first.

In this tutorial, we dive deep into one place where people made a lot of mistakes — header files. I have seen product code in large companies that didn’t have header files properly written and caused issues for maintenance. It definitely deserves more attention. Let’s start.

1. Why header files?

Header files provide a convenient way to put forward declarations from other files. In C++, every symbol (function, class, variable, etc.) must be declared before use. This can be done without header files like the following example. Here, main.cpp calls a function from util.cpp, all it needs is to put a forward declaration of the function in the source file (line 1).

We can compile the code using the following command to verify it worked.

The problem with this approach is in every place where we want to reuse a function from util.cpp, we have to forward declare it. If we have several files that need it, we end up putting the exact same declaration code in all of them which is a bad practice. Using a header file (util.h) can simplify things as shown in the following example where we put the declaration in util.h and include it in main.cpp.

2. What to/not to put in header files?

2.1 Functions/Variables declarations not definitions

Usually, you can’t put function/variable definitions in header files. As discussed in the previous tutorial (2. Compilation and 3. Linking), the reason is it violates the “one definition rule”. Once you start to use the header file in multiple files, at the compilation stage or linking stage, the compiler will see multiple definitions which is illegal.

2.2 Enum/Struct/Class definitions are allowed

However, we can put enum/struct/class definitions in the headers. The same definitions are allowed to be in different files. Check this example. We put a struct definition in the header file util.h.

In both main.cpp and util.cpp, we include this header file:

The above program can be compiled and linked with no error even though the struct definition exits in both. Having the struct defined in 2 files does not affect linking. This is clearly not the case like functions/variables. Why?

Turns out for enum/struct/classes, they are used differently compared to functions/variables in the compiler. The rationale is that enum, struct, class definitions are just “blueprints” or descriptions of the object/variable it intends to create. Unlike functions or variables which will be allocated a memory address, the definition of enum/struct/class itself does not require memory allocation. The compiler won’t be confused about the problem like “Here is X in address1, and another X in address2, which X should I pick?” which is a common problem that causes the compiler error out at linking stage.

It’s actually good practice to put struct/class definitions in header files that are shared across different files. It’s worth mentioning that multiple definitions of struct/class/enum in the same source file is not allowed and will cause a compile-time error like the following example where struct Time is defined twice:

So far, for the question of what to put in header files, we have covered the most commonly seen language features (i.e. variables, functions, enum, struct, class). Yet C++ is a complicated language with a lot of features and corner cases. It’s hard and cumbersome to address every possible scenario. I summarize what is allowed/recommended and not allowed/bad practice here for your reference:

What you can put in header files:

1. function/variables declarations

2. struct/class/enum definition

3. Inline functions

4. constant variables

5. templates

Things considered illegal or bad practices in header files:

1. non-inline function definitions

2. non-const variable definitions

3. unnamed namespaces

4. aggregate definitions

5. using directives

3. Header guards

There is one more important thing left — header guards. Let’s first look at a more entangled example where the inclusion relation is not as simple as previous codes.

We have 2 header files: time.h and util.h. time.h contains the definition of struct Time and a function printTime (definition in time.cpp) to print the date:

util.h contains a function “isDayTime” (definition in util.cpp) which requires an argument of type Time thus needs to include header file time.h.

main.cpp call both printTime and isDayTime.

Try compile main.cpp, you get an error:

What happened? Since main.cpp includes time.h (first time) and util.h which also includes time.h (second time), after preprocessing, main.cpp becomes:

We ended up defining the same struct twice in one file which is illegal. This is a typical “double inclusion” case where one header file (time.h) showed up twice on a file’s (main.cpp) “include list”. One possible way to rescue this specific case is to not include time.h in main.cpp. This relies on the fact that util.h already includes it. However, this is not a scalable generic solution because the creator of one file shouldn’t worry about what other files includes.

A better way to solve this is to use “header guards”.

Header guards are introduced to solve the “double inclusion” problem. It looks like:

During preprocessing, the compiler checks if the macro A_UNIQUE_HEADER_FILE_GUARD_NAME is not defined (#ifndef) at line 1. If yes (which means the macro is not defined), the preprocessor defines this macro first at line 2 and includes all the content following it. If no (macro already defined), the preprocessor will ignore all content until it reaches #endif. Let’s apply this rule to the previous example by adding header guards(TIME_H) for time.h:

This time, the preprocessor will see the following after put in all header files in main.cpp

The first block within the header guard (TIME_H) will be included and the second one will be ignored for the reason described above. This way, no matter how many times time.h is included, only the first one will not be ignored. All the following ones are hidden by header guards. As a best practice, every header file should use header guards.

Most latest C++ compilers have support for #pragma once which is a simplified way to express the same idea as header guards and get used a lot. In that case, we can simplify the file time.h as:

“#pragma once” provides the same functionality as header guards and requires less code. In addition, users also don’t have to pick a unique name for every header file. However, this is not part of C++ standard. Use it only when you are sure your companies’ compiler supports it.

Summary

In this tutorial, we dived into header files and learned:

  1. Header files provide a better way to put forward declarations.
  2. What should and what shouldn’t be put in header files.
  3. Always use the header guards when creating a header file.

Reference

[1] Header files (C++)

[2] 2.11 — Header files

[3] Headers and Includes: Why and How — C++ Forum

--

--