Headers and Translation Units in C++

Have you ever tried compiling a program, and the compiler shouts at you with a mysterious error like error: multiple definition or error: undefined reference? And you have no idea what it's trying to tell you, but someone tells you to slap on an inline or move some code around, and it starts working. You say to yourself, "Huh, weird", and move on. But what's actually going on?

Understanding how the C++ compiler compiles code is very useful for understanding various compiler and linker errors. It's something I wish I understood sooner, myself.

In this article, I will explain how the compiler handles headers and cpp files when compiling, as well as various common problems and mistakes, and how to solve them. It is mainly aimed at somewhat new C++ programmers, but I will assume knowledge about things like functions and classes. For simplicity I will not get into things like C++20 modules, dynamic linking, or internal linkage.

Compilation overview

First, let's look at an overview of the steps the compiler takes to compile your code, and then we'll dive into each part individually.

  1. For each cpp file (separate from any other cpp file):
    1. Run the preprocessor, which will copy and paste each header file that is #included.
    2. Compile the cpp file into an object file.
  2. Link all object files together into a single executable.

Let's dive deeper!

Translation Units

So far, I've talked about "cpp files", because the most common file extension used for them is .cpp, though .cc is also used sometimes. What I really meant was "Translation Units" (TUs). A TU is basically each "thing" that the compiler compiles separately into what is called an object file. All object files (and static libraries, which are basically just a collection of object files in a single file) are later linked together into the final executable binary (more on linking later). Headers are not TUs. They are not compiled directly by the compiler. Their contents only get compiled because they are included in TUs through #include declarations. C++20 modules are also TUs, but as previously mentioned, I will not go into them in this article. For the purpose of this article, you can think of a translation unit as equivalent to a cpp file. One translation unit for each cpp file. The fact that each TU is compiled in isolation is very important, and understanding the consequences of this will give you an understanding of why many common problems occur, and how to fix them.

Headers

As mentioned in the overview, #include is basically just a copy and paste. Let's look at a simple example!

//Header.h
#pragma once

int GetNumberOfGeese()
{
  return 12;
}
//Example.cpp
#include "Header.h"

int main()
{
  return GetNumberOfGeese();
}

The #pragma once is called a header guard. We will get to why you need this later on.

In this example, the compiler will first preprocess example.cpp, which will basically turn it into

// Included from Header.h
int GetNumberOfGeese()
{
  return 12;
}
// End of include of Header.h

int main()
{
  return GetNumberOfGeese();
}

This gives Example.cpp access to the contents of the header, all through the magic of... just copying text!

Headers are how we communicate between translation units. If you want something to be accessible in multiple TUs, it should usually go in a header so it can be included in both.

Linking

So, now we have compiled our TUs into object files, and we want to combine those object files into a single executable. This is called linking, and is a separate step from compiling. In every day conversation, we often call the whole thing "compiling", taking our code from text files all the way to an executable, but in C++, compiling is technically just the part where we turn TUs into object files. This can get confusing sometimes! Terms like "building" can be used to describe the complete process, to make things a bit clearer.

Linking is a bit complicated and I won't go too deep into details, but basically, the linker has to take all the object files, find all the places where one object file references something from a different TU, and stitch it all together so the final program works.

Declarations and Definitions

So now we know how the compiler actually compiles our code. Now we will go into what this actually means for the language itself, and for you as the programmer, starting with declarations and definitions. You should have already used and written these, so you should be at least somewhat familiar with them. But you may not have looked at them from the perspective of how the compiler actually works, which we just explored in this post.

To simplify greatly, a declaration tells the compiler that something exists, while a definition tells the compiler what that thing actually is. I think it's easiest to look at examples. We'll look at functions and types separately, since they have some important differences. We'll skip over variables for now.

Functions

This is a function declaration:

float Square(float n);

It tells the compiler "There is a function named Square which takes a single argument of type float and returns an object of type float". But it doesn't say what the function actually does. (You could take a guess from the name, but that's beside the point!) All it says is that the function exists, somewhere. Meanwhile, the following is a function definition:

float Square(float n)
{
  return n*n;
}

The important thing to know here is that in order to call a function, all the compiler needs to know is the declaration. In other words, while a declaration has to be in our TU (usually via a header), the actual definition could be in a completely different TU. As long as the linker can find it later on, it's fine. During linking, the linker will find all the calls to the function and make sure that they correctly point to the actual implementation. A definition is also a declaration, so in some cases you will only need the definition and not any separate declaration.

If you try to call the function and the linker can't find the function definition anywhere, the program will fail to link with some kind of "undefined reference" error message. Also worth mentioning is that the function definition should only exist in a single TU, or you will get "multiple definition" errors - the linker found several versions of the function definition and doesn't know which is the "real" one. It doesn't matter that they all happen to look the same, it's still an error. This is called the "One Definition Rule" (ODR). There are exceptions to this, which we will talk more about in the sections about inlining and templates below.

All of this means that usually, function declarations go in headers, while their definitions go in cpp files. If you come from a language like Python or C#, you may have wondered why C++ "splits up" functions between files - this is why.

Types

This is a class forward declaration (the only difference between classes and structs is the default access modifier, so all of this applies exactly the same to structs too):

class Goose;

This tells the compiler that there is a class named Goose somewhere, but it doesn't say anything else about it. Note that we don't usually call this a "class declaration", but rather a "forward declaration".

This is a class definition:

class Goose
{
public:
  float speed = 1.0f;
  int angerLevel = 100;

  void CauseChaos();
};

This tells the compiler exactly what the class looks like. As you can see, it also contains a member function declaration. That function will also need to be defined somewhere, just like the Square example above. You can define it directly inside the class definition if you want, or you can define it outside the class, usually in a cpp file.

If the compiler has only seen a forward declaration but not the definition of a class, you can declare pointers and references to that class, but you can't create objects of it. You also can't access any of the data members (speed and angerLevel in the class above) or member functions of the class, because the compiler doesn't know that those exist. But to create a pointer or reference, all the compiler has to know is that the type exists in the first place. A pointer or reference to something looks the same to the compiler regardless of the definition of the type, so the definition is not required. A type which the compiler knows exists but does not know the definition of is called an incomplete type.

Declaring this function is allowed even if the compiler has only seen a forward declaration of Goose:

float CalculateExpectedDamages(const Goose& goose);

However, the function definition will want to have access to the actual class definition, since it will need to access the object's data members to calculate how much damage the goose is likely to do. (The declaration float CalculateExpectedDamages(Goose goose); would also be allowed when Goose is incomplete, but would not be callable without access to the definition.) A common pattern you see is something like:

//Damages.h
#pragma once

class Goose; // Forward declaration

float CalculateExpectedDamage(const Goose& goose);
//Damages.cpp
#include "Damages.h"
#include "Goose.h" // Contains Goose definition

float CalculateExpectedDamage(const Goose& goose)
{
  return goose.speed * goose.angerLevel; //Geese do more damage the faster and angrier they are
}

Because we included the Goose.h header in the cpp file, we can access the data members of the class in the function definition, while the function declaration only needed the forward declaration of the class. In this case we could have just included the Goose.h header in the Damages.h header and skipped the forward declaration, but forward declarations like this can help reduce compile times (because it limits how many translation units the header is included in) and break circular includes (more on those later).

Similarly, this struct

class Goose;

struct GooseWatcher
{
  Goose* goose{};
};

doesn't need to know exactly what the Goose class looks like - the pointer would look the same to the compiler regardless. However, if you ever want to access goose->speed; or something like that, then knowledge of the class is needed, and the compiler has to know the definition. On the other hand, this struct

#include "Goose.h"

struct GooseHolder
{
  Goose goose{};
};

does need to have access to the class definition, because it has to know the size of the Goose object to calculate its own size, and also how to construct a Goose.

A notable difference between function definitions and class/struct definitions is that while normally, there should only be a single definition of a function within your whole program (with some exceptions, discussed later), class and struct definitions can show up once per TU (as long as they all match exactly), so types can (and usually should) be defined in headers.

Header guards

When explaining headers above, we added the line #pragma once at the top of the file. This is called a header guard. We need a header guard in case we for whatever reason need to indirectly include the same header file multiple times in a single TU. For example, we could have one header, HeaderA.h, which includes Header.h, and another header, HeaderB.h, which also includes that same header. We might then have our example.cpp file, which needs to include both HeaderA.h and HeaderB.h for whatever reason. Remember, including is just copy+paste, so

#include "HeaderA.h" // Includes Header.h
#include "HeaderB.h" // Also includes Header.h

just pastes the contents of both headers right after one another. Without a header guard, that would mean that Header.h is also pasted twice, since it was included in turn by both of the other headers. This would most likely cause redefinition errors in the compiler, as the compiler sees the same symbols (classes, functions, whatever) being defined more than once within the TU. A header guard makes sure that any one header is only ever pasted into a TU once. But keep in mind that each TU is compiled completely in isolation, so even with header guards, your headers could still be parsed and compiled several times, once per TU where they are included!

There are two types of header guards that both achieve the same goal. The #pragma once shown above is one, and the other is a macro based version:

#ifndef HEADER_H
#define HEADER_H

//all file contents

#endif // HEADER_H

This one only uses actual existing language features. #pragma once on the other hand is not part of the C++ standard, and is just an extension provided by compilers. However, all major compilers nowadays do support it, so using it is more or less completely portable. The advantages of the #pragma version include the fact that it's just a single line of code, and that you don't have to come up with your own unique identifier. In the macro version, someone else could add a file called header.h in a different folder or something, and accidentally reuse the same macro define. This could cause build errors (or worse, build success but incorrect behavior) if it means a header "thinks" it's already been included, but actually hasn't. On the other hand, some people have experienced bugs with #pragma once, as well. I have never experienced this, and I think when using modern compilers you would need to be doing very weird things for that to happen. All in all, I recommend #pragma once as the version that is easiest to use and hardest to mess up.

inline

The keyword inline has had a long journey. It was initially added to enable inlining, but today has little to do with it. I believe the historical context is important for understanding why it does what it does, though.

Inlining

Function calls have a little bit of overhead. Sometimes, for short time critical functions or functions that are only called very few times, it might be more efficient to do what's called "inlining". This means that instead of calling the function, the compiler inserts the function body directly into the calling function. (So for example using our Square function above, float f2 = Square(f); would effectively turn into something like float f2 = f*f;.)

In the past (things are a bit different nowadays, as we will get into), to be able to do this, the compiler had to have access to the function body - the function definition - while compiling a TU. If you follow the regular declaration/definition rules for functions, this would usually not be the case - the definition would be in a single TU, while all other TUs would only have access to the declaration. To solve this issue, the inline keyword was given the side effect of allowing you to break the regular One Definition Rule and put function definitions in headers. Over time as compilers evolved, this side effect has become the main purpose of the keyword.

inline does not mean that the function will be inlined - that is completely up to the compiler (and sometimes, as with recursive function calls, it might be impossible). The compiler can also choose to inline functions that are not marked as inline, if it is able to and deems it a good idea. Adding inline to a function can however influence the compiler's decision of whether to do the inlining or not. This effect used to be more prominent in the past, when compilers were less powerful and needed more human guidance.

Inlining is not always beneficial. While it makes the function call itself faster, too much inlining can increase the binary size which might be detrimental to performance.

Putting inline on a function basically tells the linker "You might find a definition for this function in multiple translation units. That's fine, just pick any of them - I promise they are all the exact same thing". Then at link time, the linker will "merge" all definitions into one.

Link Time Optimization

Modern compilers have the ability to do what's called "Link Time Optimization" (LTO), also known as "Whole Program Optimization", where the linker is also able to do some optimizations, in addition to the actual linking. Since the linker has access to all function definitions from all TUs, this means that it is potentially able to inline some functions that the compiler was unable to, because it lacked the definitions. This, together with the fact that compilers have become much better at determining when inlining is beneficial, is one of the big reasons why inline nowadays doesn't have very much to do with actual inlining.

The lesson to take away: Don't use inline to make the compiler inline your functions - use it where you think it makes your code better designed. If you mark too many functions inline, your compile times may increase, as each individual TU will be compiling its own versions of the functions separately. It also means that if you modify the function, all cpp files that include the header need to be recompiled. If all changes were isolated to a single cpp file however, only that file would need to be recompiled (linking needs to be redone regardless).

In-class Member Function Definitions

If a member function definition (not declaration!) is inside a class, it is automatically inline:

struct Goose
{
  void CauseChaos() // Since this function definition is inside the class body, it is implicitly inline
  {
    std::print("HONK!!!\n"); // (std::print is a C++23 feature)
  }
};

Inline Variables

The fact that inline has little do do with inlining, and all to do with putting definitions in headers, led to C++17 expanding the keyword to variables. This applies to both namespace scope variables (global variables) and static member variables. For example, without inline, the regular way to create a (non-const) static member variable is to declare it inside the class, and then define it in some cpp file. Something like this:

//Goose.h
struct Goose
{
  static int gooseCount; // Just a declaration
};
//Goose.cpp
int Goose::gooseCount = 0; // The definition

Without the definition, the linker would complain if you tried to access the variable. But with inline, we can write the variable as static inline int gooseCount = 0; right inside the class body, which turns it into a definition. The separate definition in the cpp file is no longer needed! The rule for how the linker handles the inline variable definitions is the same as for functions: The linker will "merge" all definitions into one.

Templates

Since templates are instantiated "on demand" by the compiler when first used, the compiler has to have access to the definition of a template wherever it is instantiated. They are therefore exempted from the One Definition Rule and usually have to be defined in headers. (As usual, there are exceptions, but that is outside the scope of this article.)

Common Problems and Common Solutions

As a general tip when trying to find the cause of issues like this, first check if the error is coming from the compiler or the linker. This will narrow down the possible causes quite a bit.

Multiple Definitions

A multiple definition error means that the linker (or sometimes the compiler with a "redefinition error") found more than one definition of something, and it doesn't know which one is the "real" one. It often means that you put a function definition in a header without marking it inline. If so, you can solve this by moving the definition into a cpp file, only leaving the declaration in the header. You can also add the inline keyword to it and leave it in the header.

It can also mean that two different functions or classes were accidentally named the same thing. You can of course fix that by renaming one of them, or if appropriate, put them in separate namespaces.

Lastly, it could also mean that you forgot to add a header guard to a header, leading to the header being included more than once in a single TU.

Undefined Reference

Also often referred to as "Unresolved external symbol". Undefined reference errors are issued when the linker can't find the definition of something once it's time to stitch all the object files together. This usually means that you forgot to add a definition to some function you've declared, or that you haven't added a required cpp file or static library to your build.

Also, if a function is marked inline, the compiler expects the actual definition to exist in any TU where the function is called, rather than just a declaration. If you've marked a function inline but still put the definition in a (different) cpp file, you've broken part of the inline "promise", and the compiler will yell at you. Either move the function definition to the header, or remove the inline specifier.

Another case where it can happen is if a template has been defined in a cpp file instead of a header. As previously mentioned, template definitions should be visible to the compiler when instantiated. Moving the template definitions to the header should fix this.

Undeclared Identifier

An undefined identifier error means that the compiler has not seen the declaration of something at all before its first use. Depending on the compiler, it might tell you "X was not declared in this scope" or "unknown type name X", etc. If the compiler had trouble parsing your code as a result of an undeclared identifier error, sometimes it gets confused and you'll instead see an error like "missing type specifier - int assumed" (at least MSVC - don't know about others). This is a misleading and confusing error message, and just happens as a result of how the compiler parsing is implemented. int is there for historical reasons I won't get into.

These errors are often caused by simple typos or forgetting to include a header. They can also be caused by circular includes - when two headers are trying to include each other. More on that in its own section.

Circular Includes

It might not be immediately obvious why circular includes are an issue. Imagine two headers:

// Book.h
#pragma once
#include "Bookstore.h"

struct Book
{
  bool AddToBookstore(Bookstore& store);
};
// Bookstore.h
#pragma once
#include <vector>
#include "Book.h"

struct Bookstore
{
  std::vector<Book> books;
};

(Imagine the books are goose themed.)

Now let's say we try to include Bookstore.h in our main.cpp.

#include "Bookstore.h"

int main()
{
  Bookstore store;
  Book book;
  book.AddToBookstore(store);
}

Now remember how including headers works. First we include Bookstore.h. The contents get pasted as expected. Our TU's code will look something like

// From Bookstore.h
#include <vector>
#include "Book.h"

struct Bookstore
{
  std::vector<Book> books;
};
// End of Bookstore.h

int main()
{
  Bookstore store;
  Book book;
  book.AddToBookstore(store);
}

Book.h will then also be included:

// From Bookstore.h
#include <vector>

// From Book.h
// The header guard stops Bookstore.h from being included again (without the header guards we would get multiple definition errors or infinite recursion instead)
//#include "Bookstore.h"

struct Book
{
  bool AddToBookstore(Bookstore& store);
};
// End of Book.h

struct Bookstore
{
  std::vector<Book> books;
};
// End of Bookstore.h

int main()
{
  Bookstore store;
  Book book;
  book.AddToBookstore(store);
}

C++ is parsed from top to bottom (for the most part). That means that because of the header guard, when the function AddToBookstore is declared above, the compiler has never seen Bookstore before - the definition of that struct comes later in the code. To compile this code, we have to break this circular include. There are two main ways to do this: Replace includes with forward declarations, or redesign the code. Both solutions are viable in this situation. If we change Book.h to the following:

// Book.h
#pragma once

struct Bookstore; // Forward declaration

struct Book
{
  bool AddToBookstore(Bookstore& store);
};

There is no longer any circular include. Bookstore.h still includes Book.h, but not the other way around. The cpp file that contains the definition of Book::AddToBookstore would have to add an include of Bookstore.h instead, in order to access the definition of the Bookstore struct. This of course won't cause any sort of circular include, since cpp files are not to be #included themselves.

The other solution is to redesign the code. We could for example remove Book::AddToBookstore and instead add a member function bool AddBook(Book book); to Bookstore. Then Book doesn't need to know about Bookstore at all - the dependency goes only one way. This makes sense - after all, you can have books without a bookstore, but you can't have a bookstore without books. This is usually how you want to solve a circular include, by breaking the circular dependency altogether. This tends to lead to code that is easier to follow. It's not always possible, though.

Summary

  • Translation Units, roughly equivalent to .cpp files, are compiled in isolation. Each TU is compiled into an object file.
  • #include copies and pastes the contents of a header. Headers themselves are not compiled, except by having their contents pasted into TUs, which in turn are compiled.
  • All compiled object files are linked together into a single executable.
  • Don't mark a function inline to tell the compiler you want it inlined - use it when you want to put the function definition in a header.
  • Templates go in headers.

Leave a Reply

Your email address will not be published. Required fields are marked *