Internal and External Linkage

A deeper look at the C++ linker and how it interacts with our variables and functions. We also cover how we can change those interactions, using the extern and inline keywords
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

In previous lessons, we introduced the idea of forward declaration. This allows our files to call functions that have been defined in a different file:

// greeting.cpp
#include <iostream>

void SayHello(){
  std::cout << "Hello";
}
// main.cpp
void SayHello();

int main(){
  SayHello();
}
Hello

Making this work is the job of the linker. After our files are taken by the compiler and converted to a temporary form, sometimes called object files.

After our object files have been created, the linker then combines them to create a final, cohesive package. We covered this process in more detail in our introductory course:

In this lesson, we go a little deeper into this process, introducing some pitfalls we’re likely to encounter, and how to solve them.

Linkage

Identifiers we use, such as the names of variables or classes, are often referred to as symbols. How a symbol interacts with the linker is commonly called its linkage.

Depending on the symbol, it will have one of three possible linkages by default. However, as we’ll see through this lesson, we can often manipulate our code to modify its linkage.

The three types of linkage are:

1. External Linkage

Identifiers with external linkage are available anywhere in our program. Examples of external linkage include:

  • Most global variables
  • Classes, structs, and enums
  • Free functions - i.e., functions that are not a member of a class or struct

In our initial example, SayHello() was a free function, so it had external linkage. That’s what made it accessible from a different file.

2. Internal Linkage

Identifiers with internal linkage are only accessible within the same file they are declared. The most common things we’ll create that have internal linkage by default are const and constexpr variables. However, later in this lesson, we’ll demonstrate how to give other symbols internal linkage.

3. No Linkage

Symbols defined within block scope (e.g., local variables within a function) have no linkage at all. They are only accessible within the block they are declared.

One Definition Rule (ODR)

We’re perhaps already intuitively familiar with the one-definition rule, which prevents us from having multiple definitions for the same symbol, in a way that would cause conflicts. A basic example of such a conflict is this:

int x{1};
int x{2};

But, when we consider external linkage, there are other, less obvious forms of this. The following program has a SayHello() definition in two places. They’re in two different files but, because free functions have external linkage, this is still a violation of the one-definition rule:

// main.cpp
#include <iostream>

void SayHello(){
  std::cout << "Hello";
}

int main(){ SayHello(); }
// greeting.cpp
#include <iostream>

void SayHello(){
  std::cout << "Hello";
}

This program will not link correctly, as a function called SayHello() that accepts no arguments and returns void is defined in multiple object files:

main.obj : error LNK2005: "SayHello(void)" already defined in greeting.obj

Anonymous Namespaces

When we want to give a symbol internal linkage, the main way to do this is through an anonymous namespace. Predictably, these are namespaces without names:

// greeting.cpp
#include <iostream>

namespace{
  void SayHello() { std::cout << "Hello"; }
}

Symbols within an anonymous namespace are still accessible within the same object file, but no longer accessible to other object files.

In the following example,

  • SayHello(), defined within main.cpp has external linkage
  • SayHello(), defined within an anonymous namespace within greeting.cpp has internal linkage and is therefore only accessible to greeting.cpp
  • Greet(), defined within greeting.cpp has external linkage and is therefore accessible to main.cpp
//main.cpp
#include <iostream>

void SayHello(){
  std::cout << "Hello from main.obj";
};

void Greet();

int main(){ Greet(); }
// greeting.cpp
#include <iostream>

namespace{
  void SayHello(){
    std::cout << "Hello from greeting.obj";
  }
}

void Greet(){ SayHello(); }

This program is no longer violating the one-definition rule, so compiles and runs successfully:

Hello from greeting.obj

Explicitly Accessing Global Symbols

In the previous example, main.cpp was defining SayHello() in the global namespace, with external linkage. As such, that definition is still accessible within greeting.cpp.

However, within greeting.cpp, calls to SayHello() will instead use the most local definition of that symbol. That is the definition within the anonymous namespace of that same file.

When two different definitions of a symbol are accessible within the same scope, the more local definition is said to be masking or shadowing the other definition.

In scenarios like this, we can specify which definition of SayHello() we’re referring to using the scope resolution operator, ::.

When the scope we’re trying to access is the global scope, we use the unary form of this operator - for example, ::SayHello().

Below, main.cpp is unchanged from the previous example - it is calling the externally linked Greet() function defined within greeting.cpp as before.

However, Greet() is now using the unary :: operator to call the externally linked SayHello() function defined within main.cpp:

// greeting.cpp
#include <iostream>

// Forward declaration
void SayHello(); 

namespace{
  void SayHello(){
    std::cout << "Hello from greeting.obj";
  }
}

void Greet(){
  // Unary :: operator
  ::SayHello();
}
Hello from main.obj

The extern Keyword

Similar to how we can "forward-declare" a function that will be defined elsewhere, so too we treat variables in the same way.

To declare that a variable will be defined externally, we mark it with the extern keyword. Below, main.cpp uses the externally-linked definition of Pi, defined within math.cpp:

//main.cpp
#include <iostream>

extern float Pi;

int main(){ std::cout << "Pi: " << Pi; }
// math.cpp
float Pi{3.14f};
Pi: 3.14

We can explicitly prepend extern to function declarations too, if preferred:

extern void SomeFunction(int Arg);

But, as we’ve already seen in our forward declaration examples, this isn’t necessary. The compiler can implicitly differentiate between a function declaration and a function definition, based on whether or not a function body is provided.

Linkage of Constants

Unlike regular variables, const and constexpr variables have internal linkage by default. This can be changed by marking the variable definition as extern:

// math.cpp
extern const float Pi{3.14f};

In the file where we’re using the global variable, we need to match the const-ness within our forward declaration. For example, if the definition was a const, our forward declaration must also be marked as const:

//main.cpp
#include <iostream>

extern const float Pi;

int main(){ std::cout << "Pi: " << Pi; }
Pi: 3.14

Visual Studio extern Error: constexpr object must be initialized

The C++ standard allows constexpr variables to be declared as extern, but Visual Studio does not currently allow this by default.

To make Visual Studio conform to the standards here, the /Zc:externConstexpr option needs to be set in the compiler options. The Visual Studio documentation has instructions on how to do this.

Inline Variables

In larger projects, if we’re using global variables at all, we’ll want to define them in one place. This is typically a header file, which we can then #include wherever it’s needed.

However, this soon leads us to break the one-definition rule, as this simple example demonstrates:

//main.cpp
#include <iostream>
#include "globals.h"

int main(){ std::cout << "Global: " << Global; }
// somefile.cpp
#include "globals.h"
//globals.h
#pragma once

int Global{42};

Now, our translation units (our source files after the preprocessor has implemented all our directives) will have multiple global definitions of the Global variable:

main.obj : error LNK2005: "int Global" already defined in somefile.obj

We have several ways to circumvent this but, as of C++17, the simplest is to declare the variable as inline:

//globals.h
#pragma once

inline int Global{42};

With no other changes, our program now works:

Global: 42

In this case, the inline keyword has the effect of having only one object file maintain the definition of Global. Every other object file has the replaced with an extern int Global; declaration.

Since all the definitions originate from the same place - the globals.h file - it doesn’t matter which definition is kept. All the definitions were equivalent.

Summary

In this lesson, we explored linkage, focusing on how extern, inline, and anonymous namespaces affect the visibility and accessibility of symbols across different translation units.

Main Points Learned

  • Forward declaration allows for the use of functions defined in different files, facilitated by the linker.
  • Symbols, or identifiers, interact with the linker based on their linkage: external, internal, or no linkage.
  • External linkage makes symbols available throughout the program, while internal linkage restricts them to the same file.
  • The One Definition Rule (ODR) prevents multiple definitions of the same symbol that could lead to conflicts.
  • Anonymous namespaces provide a way to give symbols internal linkage, limiting their scope to the containing file.
  • The extern keyword is used to declare that a variable is defined externally, allowing for its use across different files.
  • const and constexpr variables have internal linkage by default, a behavior that can be overridden with extern.
  • Inline variables, introduced in C++17, help avoid violations of the ODR when definitions are included in header files, ensuring a single instance across multiple translation units.
  • Understanding the scope resolution operator :: can help access global symbols when local definitions shadow them.

Was this lesson useful?

Next Lesson

Static Class Variables and Functions

A guide to sharing values between objects using static class variables and functions
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Internal and External Linkage

A deeper look at the C++ linker and how it interacts with our variables and functions. We also cover how we can change those interactions, using the extern and inline keywords

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Objects, Classes and Modules
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Static Class Variables and Functions

A guide to sharing values between objects using static class variables and functions
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved