Limiting Visibility with Internal Linkage
Restrict variables and functions to a single translation unit using static and unnamed namespaces.
What Is Internal Linkage?
An identifier has internal linkage when it can be used anywhere in the translation unit that defines it, and nowhere else. The name works normally for the whole file. It simply stops at the file's edge.
The surprising half is what that means for duplicates. Two source files may each define static int g_sampleCount, and the result is two entirely separate variables that happen to spell their names the same way. Neither file can see the other's, and the program links without complaint.
The Question Linkage Answers
A C++ program is compiled one translation unit at a time. Each source file, with its headers pasted in by the preprocessor, is turned into an object file on its own, and only afterwards does the linker try to join those object files into a program. Linkage is the property that decides what the linker gets told about a name.
| Linkage | Where the name can be used | What the linker can do with it |
|---|---|---|
| no linkage | inside its own block only | nothing; the name never leaves the function |
| internal linkage | anywhere in its own translation unit | nothing; it cannot be matched to another file |
| external linkage | any translation unit that declares it | connect uses in one file to a definition in another |
Local variables, from the Local variables lesson, sit in the top row: they have no linkage at all. Global variables and functions sit in one of the other two rows, and this lesson is about the middle one.
Internal linkage is not a scope rule. The name is still visible for the rest of the file after its definition, exactly like any other global. What changes is whether the name is offered up for cross-file matching. A name with internal linkage is either missing from the object file's symbol table entirely or marked as belonging to that object file alone, so the linker will never use it to satisfy a reference coming from somewhere else.
What Every Global Name Starts With
Some names need nothing done to them, because they begin with internal linkage. For variables, constness is what decides. Functions have no equivalent switch: they always start out external.
| Definition at namespace scope | Linkage with no keyword added | To seal it inside the file |
|---|---|---|
int g_readingsToday{18}; |
external | add static |
const int g_pressureFloorHpa{950}; |
internal | already sealed |
constexpr int g_sensorChannels{4}; |
internal | already sealed |
void reportReading(int hpa) { ... } |
external | add static |
A global variable that carries internal linkage is often called an internal variable. Sealed and unsealed globals coexist happily in one file:
#include <iostream>
int g_readingsToday{18}; // non-const global: external linkage, another file could reach this
static int g_calibrationDriftHpa{-3}; // static seals the name inside this file
const int g_pressureFloorHpa{950}; // const globals are sealed already
static constexpr int g_sensorChannels{4}; // static is allowed here, and changes nothing
int main()
{
std::cout << g_readingsToday << ' ' << g_calibrationDriftHpa << ' '
<< g_pressureFloorHpa << ' ' << g_sensorChannels << '\n';
return 0;
}
This prints:
18 -3 950 4
Writing static on the last line is redundant rather than wrong: it asks for the linkage the constant already has. The reason const works this way is worth knowing. A constant is meant to be usable in constant expressions, which requires the compiler to have seen its definition, and it is meant to be shareable through a header. Internal linkage delivers both at once, because every file that includes the header gets its own definition and no two of them collide.
When
static appears on a global like this it is acting as a storage class specifier: a keyword that sets a name's linkage and its storage duration in a single stroke. static, extern, and mutable are the three you are likely to meet.
The same keyword means something different inside a function. A local variable declared
static is not about linkage at all, since locals have none; it is about lifetime. The Static local variables lesson covers that meaning.
Two Files, Two Variables, One Name
Sealing a name is easy to assert and easy to doubt, so it is worth watching two files disagree about the value of the same identifier.
The examples below are made of two source files, so the in-browser runner, which builds a single file, cannot run them. Save both files in one directory and hand them to the compiler together, for example
g++ -std=c++20 main.cpp barometer.cpp -o station.
barometer.cpp:
#include <iostream>
static int g_sampleCount{7}; // this g_sampleCount belongs to barometer.cpp and nothing else
void reportBarometerCount()
{
std::cout << "barometer.cpp sees " << g_sampleCount << '\n';
}
main.cpp:
#include <iostream>
static int g_sampleCount{2}; // an unrelated variable that happens to share the name
void reportBarometerCount();
int main()
{
reportBarometerCount();
std::cout << "main.cpp sees " << g_sampleCount << '\n';
return 0;
}
Compiled together, this prints:
barometer.cpp sees 7
main.cpp sees 2
Two definitions, two values, one program, no error. Each function reads whichever g_sampleCount its own file defines, and neither file has any way of naming the other's. Drop the static from both and the program stops linking, because there would then be two definitions of one external variable.
Why This Does Not Break the One-Definition Rule
The one-definition rule from the Forward declarations and definitions lesson says an object or function may not be defined more than once, whether inside a file or across the program. The example above looks like a direct violation of the second half.
It is not, because the rule counts entities, not spellings. Internal objects and functions defined in different files are separate entities even when their names and types match exactly, so the program above contains two variables with one definition each rather than one variable with two. Nothing is duplicated because nothing is shared.
Sealing a Helper Function
Functions get internal linkage the same way, by writing static on the definition. This is where the feature earns its keep: a file can keep its working parts to itself and expose only what callers are meant to use.
barometer.cpp:
#include <iostream>
constexpr int g_pressureFloorHpa{950}; // internal linkage by default
static int clampToFloor(int hpa) // internal linkage on request: a helper no other file may call
{
return (hpa < g_pressureFloorHpa) ? g_pressureFloorHpa : hpa;
}
void reportReading(int hpa) // external linkage: this is the file's public face
{
std::cout << "logged " << clampToFloor(hpa) << " hPa" << '\n';
}
main.cpp:
void reportReading(int hpa);
int main()
{
reportReading(1013);
reportReading(908);
return 0;
}
Compiled together, this prints:
logged 1013 hPa
logged 950 hPa
One file, two kinds of linkage. The constant and the helper are sealed in, reportReading is the only name the rest of the program can reach, and main.cpp needs to know about nothing else.
Now watch what happens when another file tries to go around that interface. The compiler will accept any forward declaration you write, because it has no way of checking a claim about a file it is not currently reading. The following main.cpp is broken, and the pair does not link:
barometer.cpp:
#include <iostream>
constexpr int g_pressureFloorHpa{950};
static int clampToFloor(int hpa) // static: the name never leaves barometer.cpp
{
return (hpa < g_pressureFloorHpa) ? g_pressureFloorHpa : hpa;
}
void reportReading(int hpa)
{
std::cout << "logged " << clampToFloor(hpa) << " hPa" << '\n';
}
main.cpp:
#include <iostream>
int clampToFloor(int hpa); // the compiler believes this declaration
int main()
{
std::cout << clampToFloor(908) << '\n';
return 0;
}
Both files compile. Linking them fails, with the linker pointing at the offset inside main.cpp where the unresolved call sits:
main.cpp:(.text.startup+0xc): undefined reference to `clampToFloor(int)'
collect2: error: ld returned 1 exit status
Diagnostics from the linker read differently from compiler diagnostics because a different tool produces them, after the source text is gone. The substance is what matters here: clampToFloor exists, it compiled, it works, and the linker still will not hand it over. That is precisely the guarantee static was asked for.
Unnamed Namespaces Do the Same Job, Wider
Modern C++ leans away from static for this purpose and towards an unnamed namespace, which gives internal linkage to everything declared inside it:
#include <iostream>
namespace // no name, so everything inside gets internal linkage
{
int g_sampleCount{7};
void reportSampleCount()
{
std::cout << "samples so far: " << g_sampleCount << '\n';
}
}
int main()
{
reportSampleCount();
return 0;
}
This prints:
samples so far: 7
Two things make it the better tool. It covers more kinds of identifier, including type names, which cannot be marked static at all. And it scales: sealing a dozen names means one namespace rather than a dozen keywords, and the block itself documents where the file's private section begins and ends. The Unnamed and inline namespaces lesson goes into the details.
Deciding What to Seal
| What you are defining | Seal it? | Reason |
|---|---|---|
| a helper function no caller outside this file should use | yes | keeps the file's interface small and stops others depending on it |
| a global only this file's functions are supposed to maintain | yes | nothing outside the file can reach in and change it |
a const or constexpr global |
nothing to do | it is internal already |
| a function or object other files are meant to call | no | sealing it turns every outside use into a link error |
Two motives sit behind that table. The first is access control: some identifiers are implementation detail, and internal linkage makes that structural rather than a matter of politeness. The second is collision avoidance: a name that never reaches the linker can only clash with names in its own translation unit, never with something on the far side of the program.
Some style guides push this to its limit and seal everything that is not deliberately exported. That is a defensible habit if you keep it up consistently.
Give an identifier internal linkage whenever you have a concrete reason to keep other files away from it. If you want to go further and seal everything that is not part of the file's interface, reach for an unnamed namespace rather than scattering
static.
Key Terminology
Translation unit: one source file after the preprocessor has finished with it, which is the unit the compiler actually processes.
Internal linkage: the property that lets a name be used throughout its own translation unit while keeping it unreachable from every other one.
Internal variable: the usual shorthand for a global variable with internal linkage.
Storage class specifier: a keyword such as static, extern, or mutable that sets both the linkage and the storage duration of the name it is applied to.
Unnamed namespace: a namespace written without a name, which gives internal linkage to every declaration inside it.
Summary
What it does: an identifier with internal linkage is usable anywhere in the translation unit that defines it and is unreachable from any other translation unit.
Why the linker matters: files are compiled separately and joined afterwards. Internal names are not offered to the linker for cross-file matching, which is the whole of the effect.
Variable defaults: non-const globals are external and need static to be sealed. const and constexpr globals are internal already, so static on them is redundant.
Function defaults: functions are external, and static on the definition seals them. A forward declaration in another file still compiles, but the program fails to link.
Not an ODR violation: identically named internal entities in different files are distinct entities with one definition each, so nothing is defined twice.
static has two jobs: on a global it is a storage class specifier setting internal linkage; on a local variable it sets storage duration and has nothing to do with linkage.
Unnamed namespaces: preferred in modern C++ because they cover identifiers static cannot, including type names, and seal many names in one block.
When to use it: whenever you have a reason to keep other files out, and as a default for everything that is not part of a file's deliberate interface.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Limiting Visibility with Internal Linkage - Quiz
Test your understanding of the lesson.
Practice Exercises
Internal Linkage with static
Use static keyword to give global variables and functions internal linkage, limiting their visibility to the current file.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!