extern
and inline
keywordsIn previous lessons, we introduced the idea of forward declaration. This allows our files to call functions that have been defined in a different file:
// main.cpp
void SayHello();
int main(){ SayHello(); }
// greeting.cpp
#include <iostream>
void SayHello(){ std::cout << "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 connects them together to create a final, cohesive package.
Our previous SayHello()
program compiles, and links successfully to create the expected output:
Hello
In this lesson, we go a little deeper on how that works, and some pitfalls we’re likely to encounter.
How a symbol interacts with the linker is commonly called its linkage. There are three types of linkage and the things we create, like variables, functions, and classes will have one of these types of linkage by default. However, as we’ll see through this lesson, we can often manipulate our code to change that behavior.
The three types of linkage are:
Identifiers with external linkage are available anywhere in our program. Examples of external linkage include:
SayHello
function in our initial exampleClass names, most global variables, and “free functions” (i.e., functions that are not part of a class) have external linkage. In the previous example, SayHello()
was a free function, so it had external linkage. That’s what made it accessible from a different file.
Identifiers with internal linkage are only accessible within the same file they are declared. The most common thing we’ll create that has internal linkage by default are const
and constexpr
variables. However, later in this lesson, we’ll demonstrate how to give other symbols internal 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.
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{5};
int x{5};
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
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. That means our previous program is no longer violating the one definition rule:
//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(); }
Hello from greeting.obj
In this example, main.cpp
is defining SayHello()
in the global namespace. As such, that definition is still accessible within greeting.cpp
.
If we wanted to use it there, we would need to forward declare it in the usual way. Then, when we call the function, we need to specify that we want to call the symbol that is defined within the global namespace.
We do that through the scope resolution operator ::
, but without a left operand:
// greeting.cpp
#include <iostream>
// Forward declaration
void SayHello();
namespace{
void SayHello(){
std::cout << "Hello from greeting.obj";
}
}
void Greet(){
// Unary :: operator
::SayHello();
}
The unary ::
operator indicates we want to access a symbol in the global namespace which, in this case, is the definition of SayHello()
originating from main.cpp
:
Hello from main.obj
extern
KeywordSimilar to how we can “forward-declare” a function that will be defined elsewhere, so too we treat variables in the same way.
To declare a variable will be defined externally, we mark it with the extern
 keyword:
//main.cpp
#include <iostream>
extern float Pi;
int main(){ std::cout << "Pi: " << Pi; }
// somefile.cpp
float Pi{3.14f};
Pi: 3.14
const
and constexpr
VariablesUnlike regular variables, const
and constexpr
variables have internal linkage by default. This can be changed by marking the variable definition as extern
:
// somefile.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 declaration. For example, if the definition was a const
, our declaration must also be marked as const
:
//main.cpp
#include <iostream>
extern const float Pi;
int main(){ std::cout << "Pi: " << Pi; }
Pi: 3.14
extern
Error: constexpr
object must be initializedThe 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.
In larger projects, if we’re using global variables at all, we’ll want to define global variables 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.
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.