The C++ Linker

Go deeper on how the linker works, and how we can take full advantage of it by using forward declarations and incomplete types.
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

3D art showing a character in a bar
Ryan McCombe
Ryan McCombe
Posted

We previously talked about the first two steps of the build process - preprocessing and compiling.

The preprocessor scans our code for specific preprocessor directives - lines of code that start with a #. Once such a line is encountered, the preprocessor will determine what, if any, modifications it needs to make to our source code.

The modifications occur in temporary copies of our files, which then get sent off to the compiler.

The compiler takes these files, and transforms them into low level machine code, line by line. For every file that was sent to the compiler, a file of this binary code will be generated.

These files, which are sometimes called object files, are finally linked together to create the executable program our users will run.

How Linking works in C++

In the previous lesson, we had three files set up like this:

// character.h
#pragma once
class Character {
  void TakeDamage(int Damage);
}
// character.cpp
#include "character.h"

void Character::TakeDamage(int Damage) {
  // Implementation
}
// main.cpp
#include "character.h"

int main() {
  Character PlayerCharacter;
  PlayerCharacter.TakeDamage(50);
}

This setup demonstrated the linker in action.

Specifically, note the PlayerCharacter.TakeDamage(50); statement in our main file.

The implementation of this function is never provided in main.cpp, even after the preprocessor runs.

The implementation is provided in character.cpp, but none of our files have an #include directive that grabs the contents of character.cpp.

Yet, our code compiles successfully and works as expected. This was because when the compiler was building main.cpp, we had enough code in there to make it aware that Character was a class, and the Character class is going to have a function called TakeDamage.

This information was added to main.cpp before it was compiled, thanks to the #include "character.h" directive.

The compiler didn't have an implementation of the function, so it just created a temporary marker in the binary code. The marker is an instruction to the linker - it tells the linker "this function is hopefully going to be implemented in another file - when you know where it is, link it here".

The linker scans through all our object files for these temporary markers, and resolves them with the correct connection.

At the end of this process, there may still be some of these temporary markers remaining. This would mean the linker was unable to find something we're using, even after looking in all our object files.

If that's the case, the linker will fail, and alert us to the issue. It knows we've made a mistake and will throw an error at that point.

Forward Declarations

This approach of telling the compiler "Character::TakeDamage is a function - let the linker find it" is called a forward declaration.

We already saw forward declarations back in the first chapter. In that chapter, we provided the "prototype" of a function at the top of the file, and provided the implementation further down that same file.

Now, we've just expanded that idea to multiple files. The mechanism is the same, and it's the linker that makes this work.

Previously, we were forward declaring functions, but now we've seen that we can also forward declare entire classes by asking the preprocessor to include their header file.

Forward Declaring Classes

We previously introduced the pattern of keeping our declarations in a header file and our implementations in a .cpp file.

One of the benefits of this is that it gave us a much shorter header file to use with the #include directive. This meant the files our compiler received were much smaller, so our compile times were faster.

The header file contains the prototypes of our class functions, and the types of its variables. But sometimes, the compiler doesn't even need to know that much.

Lets add an Attack function to our Character as an example.

class Character {
  void Attack(Monster* Target);
};

This file is now referring to a Monster type, so we have to tell the compiler what a Monster is.

We can do that by including the entire header file for the Monster class, as we've done in the past.

#include "Monster.h"

class Character {
  void Attack(Monster* Target);
};

This will include all the variables and function prototypes for the Monster type. That header file might also be including other header files.

In this scenario, the compiler doesn't really need to know everything that is in the Monster type, it just needs to know that Monster is a type.

Therefore, we can just tell the compiler that Monster is a class using a forward declaration:

class Monster;

class Character {
  void Attack(Monster* Target);
};

This approach keeps our files even more compact, and our compilations faster.

Test your Knowledge

What is a benefit of using a forward declaration rather than an include directive?

This technique also allows us to resolve "circular dependencies". If File A needs to use something in File B, and File B needs to use something that is in File A, this is a circular dependency.

We can't resolve it by making each file #include the other, as that would create an infinite loop. A includes B, which includes A, which includes B, and so on.

Instead, using a forward declaration in one (or both) of the files can let us resolve the dependencies.

C++ Incomplete Types

By forward declaring in this way, Monster becomes an "incomplete type". This means the compiler won't know about any of its members.

We cannot do as much with incomplete types as complete types. We can't access a variable or call a function using an incomplete type, for example:

class Monster;

class Character {
  void Attack(Monster* Target) {
    // Need to #include the full header to do these
    Monster->Health;
    Monster->TakeDamage(50);
  };
};

The rules around what we can and can't do with incomplete types are something that will come with experience and some trial and error.

Test your Knowledge

What is a drawback of using a forward declaration rather than an include?

As with many things, the use of this technique is opinionated, and recommendations differ:

Avoid using forward declarations where possible. Instead, include the headers you need.

If you can use forward declarations instead of including a header, do so.

Either way, this technique is very common and we will see it a lot, particularly in graphics and game engines.

When we encounter it, we will now know what is happening, and hopefully understand a bit more about how what the linker does in general.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
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

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:

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

C++ Namespaces and Using Declarations

Learn how we can use namespaces to keep related variables, functions and classes together.
3D art showing a fantasy character
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved