Sharing Constants with Inline Variables
Define constants in headers that multiple source files can use without linker errors.
What Are Shared Global Constants?
A shared global constant is a value that the whole program has to agree on: the tick rate of a simulation, the number of players an arena holds, the build string printed in a log header. Retyping the literal 60 in every file that needs the tick rate works right up until the day it becomes 120, and then correctness depends on finding every copy.
The goal is to state each value once and have every source file refer to that one statement. C++ makes this slightly awkward, because the compiler never sees your program as a whole. It sees one translation unit at a time, and the linker assembles the results afterwards. That split is what decides which techniques work, so this lesson starts there, then walks through the three ways to publish constants program-wide.
Why Separate Compilation Complicates This
Two facts from earlier in this chapter drive everything below.
First, each source file is compiled on its own. When the compiler builds main.cpp, the only declarations and definitions it knows about are the ones physically present in that file, including everything the preprocessor pasted in from headers. Whatever sits in settings.cpp is invisible.
Second, an identifier's linkage decides what the linker does with it:
- Internal linkage: the entity belongs to its own translation unit. Two files can each have one, and the linker never compares them.
- External linkage: the entity is offered to the linker, which resolves every use across the program to a single definition.
Global variables marked const or constexpr have internal linkage by default. That default is the reason the first technique below does not immediately collapse into a duplicate definition error, and the reason the third technique needs a keyword to opt out of it.
Option 1: Define constexpr Constants in a Header
Collect the values into a namespace inside a header, and include that header wherever they are needed. The namespace is not decoration: it keeps short names like maxPlayers from colliding with anything else in the global scope, and it documents where a value came from at every use site.
settings.h:
#pragma once
namespace settings
{
constexpr int maxPlayers{ 4 };
constexpr int tickRateHz{ 60 };
constexpr double arenaRadius{ 12.5 };
}
main.cpp:
#include "settings.h"
#include <array>
#include <iostream>
int main()
{
std::array<int, settings::maxPlayers> scores{};
std::cout << "Players per arena: " << scores.size() << '\n';
std::cout << "Seconds per tick: " << 1.0 / settings::tickRateHz << '\n';
std::cout << "Arena radius: " << settings::arenaRadius << '\n';
return 0;
}
Output:
Players per arena: 4
Seconds per tick: 0.0166667
Arena radius: 12.5
Notice that settings::maxPlayers works as the length of a std::array. The compiler read the initializer { 4 } in this very translation unit, so the value is available at compile time. Keep that in mind, because the next option loses it.
What actually happens at build time is that the preprocessor copies the text of the header into every including file, so each translation unit compiles its own set of definitions. They coexist peacefully because internal linkage keeps them private, and in most cases the compiler folds the value straight into the instructions and emits no variable at all.
You can watch the duplication if you force the constants into memory by taking their addresses from two different files.
report.cpp:
#include "report.h"
#include "settings.h"
const int* addressSeenByReport()
{
return &settings::maxPlayers;
}
main.cpp:
#include "report.h"
#include "settings.h"
#include <iostream>
int main()
{
const int* fromMain{ &settings::maxPlayers };
const int* fromReport{ addressSeenByReport() };
std::cout << "Same object in both files: " << (fromMain == fromReport ? "yes" : "no") << '\n';
return 0;
}
Output:
Same object in both files: no
Two files, two objects. A common misconception is that #pragma once or a header guard prevents this. It does not: those stop a header from being pasted into the same file twice, and say nothing about the header being pasted into fifty different files once each.
That leaves two costs. Editing the header forces a rebuild of every file that includes it, which is felt as soon as the project is large or the values are still being tuned. And when a constant is big enough that the compiler cannot fold it away, such as a large array, every translation unit pays for its own copy.
Option 2: Declare in the Header, Define in One Source File
To get exactly one object, move the definitions into a source file and leave only declarations in the header. The extern keyword on the definitions overrides the internal linkage default, so the linker can see them.
settings.cpp:
#include "settings.h"
namespace settings
{
extern constexpr int maxPlayers{ 4 };
extern constexpr int tickRateHz{ 60 };
extern constexpr double arenaRadius{ 12.5 };
}
settings.h:
#pragma once
namespace settings
{
extern const int maxPlayers;
extern const int tickRateHz;
extern const double arenaRadius;
}
The declarations sit in the same namespace as the definitions, otherwise they would name different entities. They are declared const rather than constexpr because a declaration cannot be constexpr: there is no initializer for the compiler to evaluate.
Every use now resolves to the single object created in settings.cpp, and changing a value recompiles that one file instead of the whole dependency tree. The price shows up the moment a constant is needed at compile time:
#include "settings.h"
#include <array>
#include <iostream>
int main()
{
std::array<int, settings::maxPlayers> scores{};
std::cout << scores.size() << '\n';
return 0;
}
main.cpp: In function 'int main()':
main.cpp:8:41: error: the value of 'settings::maxPlayers' is not usable in a constant expression
8 | std::array<int, settings::maxPlayers> scores{};
| ^
In file included from main.cpp:1:
settings.h:5:22: note: 'settings::maxPlayers' was not initialized with a constant expression
5 | extern const int maxPlayers;
| ^~~~~~~~~~
A declaration promises that an object exists somewhere. Only a definition tells the compiler what the object holds. A template argument or an array length needs the value itself, so it needs the definition to be visible in the translation unit that uses it.
Inside settings.cpp these constants are still full constant expressions. Everywhere else the compiler has only the declaration, so the value has to be fetched at run time, which also means the optimizer has less to work with. This is the underlying reason a constexpr variable cannot be split across a header and a source file the way a function can: whatever is meant to be usable at compile time has to live in the header.
The second cost is bookkeeping. The declaration and the definition are separate pieces of text in separate files, and nothing forces you to keep the type and spelling of one in step with the other.
Option 3: Define inline constexpr Constants in a Header (C++17)
C++17 removes the tradeoff. As covered in the previous lesson, an inline variable may be defined in many translation units as long as every definition is identical, and the linker collapses them into one object. Applied to constants, that means the definition can stay in the header, where the compiler can see the value, while the program still ends up with a single object.
settings.h:
#pragma once
namespace settings
{
inline constexpr int maxPlayers{ 4 };
inline constexpr int tickRateHz{ 60 };
inline constexpr double arenaRadius{ 12.5 };
}
Nothing changes at the use site, and the std::array from Option 1 still compiles, because the initializer is right there in the header. What does change is the address test:
Same object in both files: yes
Two details are worth pinning down.
constexpr functions are implicitly inline, but constexpr variables are not. If you want an inline constant, you have to write inline yourself.
By default an inline variable is given external linkage, which is precisely what makes deduplication possible: the linker can only merge definitions it is allowed to see. A plain constexpr variable keeps internal linkage, so its per-file copies stay hidden from the linker. That is why Option 1 is not an ODR violation, and also why it cannot produce a single shared object.
One cost carries over from Option 1: the definitions still live in a header, so editing that header still triggers a rebuild of everything that includes it.
Constant Strings
Text constants follow the same pattern, with std::string_view as the type. It is a read-only view over the string literal, so it can be constexpr, unlike std::string.
settings.h:
#pragma once
#include <string_view>
namespace settings
{
inline constexpr int maxPlayers{ 4 };
inline constexpr std::string_view buildName{ "arena-1.4.0" };
}
main.cpp:
#include "settings.h"
#include <iostream>
int main()
{
std::cout << "Build " << settings::buildName << " supports " << settings::maxPlayers << " players\n";
return 0;
}
Output:
Build arena-1.4.0 supports 4 players
Choosing an Approach
| Objects created | Usable in constant expressions | Rebuild when a value changes | Requires | |
|---|---|---|---|---|
| constexpr in header | One per including file | Everywhere | Every including file | Any standard |
| extern in source file | One | Only inside the defining file | One file | Any standard |
| inline constexpr in header | One | Everywhere | Every including file | C++17 |
On C++17 or later, define global constants as inline constexpr in a header. It is the only option that gives you a single object and compile-time usability at the same time.
Two situations justify something else. On a codebase pinned to an older standard, Option 1 is the practical choice, and its duplicate objects are usually optimized away anyway. And if you are actively tuning a handful of values and the rebuild fan-out is slowing you down, moving just those values into a source file with Option 2 is a reasonable temporary trade, provided nothing needs them at compile time.
Scope, duration, and linkage are collected into a single reference table in the chapter summary lesson.
Summary
The problem: constants that several files depend on should be written once, not copied into each file, so that changing a value is a single edit.
Why it takes thought: each translation unit is compiled independently, so the compiler only sees what is in the current file plus its headers, and linkage decides what the linker does with what is left.
Option 1, constexpr in a header: simple and works in any standard. Every including file gets its own copy with internal linkage, which is legal but means duplicated objects when the values cannot be folded away, plus a full rebuild of includers on every edit.
Option 2, extern in a source file: produces exactly one object and limits recompilation to a single file, but outside that file the compiler sees only a declaration, so the constants stop being constant expressions and cannot be used as array lengths or template arguments.
Option 3, inline constexpr in a header (preferred): one object shared program-wide and full compile-time usability everywhere, at the cost of C++17 and the usual header rebuild fan-out.
Two rules to remember: constexpr variables are not implicitly inline even though constexpr functions are, and inline variables have external linkage so the linker can merge their definitions, while non-inline constexpr variables have internal linkage and stay private to their file.
Strings: use std::string_view for constant text.
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.
Sharing Constants with Inline Variables - Quiz
Test your understanding of the lesson.
Practice Exercises
Inline Global Constants
Use inline constexpr to create global constants that can be shared across multiple files.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!