How Extern Works Across Translation Units in C++

In C++, the extern keyword extends the visibility of variables and functions beyond the boundaries of a single translation unit. By declaring a symbol with extern, you inform the compiler that the symbol exists and has storage allocated elsewhere, deferring the resolution of its actual memory address to the linking stage. This mechanism is essential for sharing global state, accessing cross-module functions, and preventing multiple-definition errors under the One Definition Rule (ODR).

Translation Units and Compilation

A translation unit (TU) consists of a single source file (.cpp) along with all the headers directly or indirectly included in it. The C++ compiler processes each TU independently, converting source code into an object file (.o or .obj). Because the compiler operates on one TU in isolation, it cannot locate identifiers defined in a completely separate source file without explicit guidance.

Declarations vs. Definitions

Understanding extern requires a distinction between a definition and a declaration:

  • A definition specifies the type, reserves storage (allocates memory), and potentially initializes the symbol. Under C++'s One Definition Rule, a non-inline variable or function can only have one definition across the entire program.
  • A declaration merely announces the existence, name, and type of a symbol to the compiler without allocating storage.

When applied to a variable, extern acts strictly as a declaration, provided it is not accompanied by an initializer:

// Translation Unit A: defining the variable
// file_a.cpp
int globalCounter = 42; // Definition with external linkage

// Translation Unit B: referencing the variable
// file_b.cpp
extern int globalCounter; // Declaration: informs compiler that storage is elsewhere

void increment() {
    globalCounter++;
}

If you initialize an extern variable (e.g., extern int x = 10;), it is treated as a definition, defeating its use as an external reference within that scope.

The Role of the Linker

When compiling file_b.cpp, the compiler encounters globalCounter and verifies its type against the extern declaration. Because there is no memory address yet assigned to it within that unit, the compiler generates a placeholder entry in the object file’s symbol table, categorizing the symbol as an unresolved external reference.

Once all TUs are compiled into object files, the linker combines them into an executable or shared library. The linker inspects the exported symbol table of file_a.o (which defines globalCounter) and updates the placeholder in file_b.o with the actual memory address assigned to globalCounter. If the definition cannot be found in any translation unit, the linker raises an "undefined reference" error. If defined in more than one unit, it raises a "duplicate symbol" error.

Functions and Internal Linkage Nuances

Functions in C++ have extern linkage by default. Declaring void printMessage(); in a header file is implicitly extern void printMessage();. The extern keyword is therefore rarely written out for functions unless dealing with language linkage.

However, non-local variables behave differently, particularly const and constexpr variables. In C++, a const global variable defaults to internal linkage (static visibility), meaning it cannot be referenced outside its own translation unit. To expose a const variable to another TU, you must explicitly mark both the definition and the declaration with extern:

// constants.cpp
extern const int MaxLimit = 100; // Definition with external linkage

// main.cpp
extern const int MaxLimit;       // Declaration

Language Linkage: extern "C"

In addition to linkage visibility, extern is used to specify language linkage, primarily with extern "C". C++ employs name mangling to encode parameter types into function symbol names to support function overloading. The C language does not use name mangling.

Using extern "C" instructs the C++ compiler to generate unmangled symbol names for the specified functions, enabling C++ translation units to reference functions compiled in pure C object files, or allowing C code to consume C++ definitions.