What Every C++ Developer Should Know to (Correctly) Define Global Constants
Constant values are an everyday tool to make code more expressive, by putting names over values.
For example, instead of writing 10
you can write MaxNbDisplayedLines
to clarify your intentions in code, with MaxNbDisplayedLines
being a constant defined as being equal to 10
.
Even though defining constants is such a basic tool to write clear code, their definition in C++ can be tricky and lead to surprising (and even, unspecified) behaviour, in particular when making a constant accessible to several files.
Everything in this article also applies to global variables as well as global constants, but global variables are a bad practice contrary to global constants, and we should avoid using them in the first place.
Thanks a lot to Patrice Roy for reviewing this article and helping me with his feedback!
Declaring a global constant: the natural but incorrect way
To define a constant of type X
, the most natural way is this:
X const x;
Note: Maybe it would seem more natural for you to read const X x
. Even though I’m an East const person, none of the contents of this post has anything to do with putting const before or after the type. Everything here holds with const X x
(friendly hat tip to the folks on the West side of the const
).
This works ok (assuming that X
has a default constructor) when X
is defined and used only inside a .cpp
file.
But what if X
is defined this way in a header file, which is #include
d in several .cpp
files?
This code compiles, but doesn’t define a global constant!
Rather, it defines two global constants. How so? The preprocessor #include
directives essentially copy-paste the code of header.h
into each .cpp
file. So after the preprocessor expansion, each of the two .cpp
file contains:
X const x;
Each file has its own version of x
. This is a problem for several reasons:
- for global variables, it is undefined behaviour (objects must be defined only once in C++),
- for global constants, since they have internal linkage we’re having several independent objects created. But their order of initialisation is undefined, so it’s unspecified behaviour,
- it uses more memory,
- if the constructor (or destructor) of
X
has side effects, they will be executed twice.
Strictly speaking, the undefined behaviour makes the last two reasons rather theoretical, because in undefined behaviour anything can happen. But if the two objects are created, then they would consume more memory and two constructors (and destructors) would be called.
Really?
Given that writing X const x
is such a natural thing to do (another hat tip to the const
Westerners), you may doubt that such problems could appear. I doubted that too.
Let’s make a simple test to observe it with our own eyes: let’s add a side effect in the constructor of X
:
class X { public: X(){ std::cout << "X constructed\n"; } };
With this addition, here is what our program with the two .cpp
files outputs:
X constructed X constructed
Wow. This was real. x
is constructed twice.
How to fix it then?
In C++17: inline variables
C++17 offers a “simple” solution to this. (I write “simple” between quotes because even if it is simpler than the solution before C++17, the real simplest way should be the natural above way. Which doesn’t work. This feature of C++ makes the language a little harder to learn).
The solution in C++17 is to add the inline
keyword in the definition of x
:
inline X const x;
This tells the compiler to not to define the object in every file, but rather to collaborate with the linker in order to place it in only one of the generated binary files.
Note that this usage of inline
has (to my knowledge, correct me if I’m wrong in the comments section) nothing to do with copying code at call site, like with inline
functions.
With this change our program now correctly outputs:
X constructed
inline
and class constants
Constants inside of a class, declared static
, have the same scope as global constants, and inline
simplified their definition in C++17 too.
Before C++17, we had to follow the annoying pattern of declaring the static
in the class definition, and define it outside in only one cpp
file:
// header file class X { static std::string const S; }; // in one cpp file std::string const X::S = "Forty-Two";
With inline
, we can define it and declare it at the same time:
// header file class X { static inline std::string const S = "Forty-Two"; }; // cpp file // nothing!
But not everyone compiles their code in C++17, at least at the time of this writing. How to share a global constant across multiple files before C++17?
Before C++17: the extern
keyword
Before C++17, one way to fix the problem is to use the extern
keyword in the header file:
extern X const x;
It looks somewhat similar to inline
, but its effect is very different. With extern
, the above code is a declaration, and not a definition. With inline
, it was a definition. This declaration informs all the #include
ing files of the existence and type of x
.
Even if C++ requires a unique definition of each object, it allows multiple declarations.
However, to use x
we need to define it somewhere. This can be done in any of the .cpp
files. You are the one to decide in which file in makes more sense to define it, given the meaning of your global constant, but it will work with any files:
This way our program outputs:
X constructed
x
is constructed only once.
And since the line in the header is only a declaration, it doesn’t contain the call to the constructor. This shows when the constructor of X
can accept values:
Note how the declaration in the header file doesn’t take constructor arguments, while the definition in the .cpp
file does.
Note that for this to work, there needs to be exactly one definition of x
. Indeed, if there is no definition we get an undefined external symbol error, and if there is more than one there is a duplicate external symbol.
As for constants inside of classes, there are no other solution than resorting to the annoying pattern of defining the constant outside of the class in one cpp file.
static
is not a good solution
static
has several meanings in C++. When we’re not talking about a class constant, declaring an object or function static
defines it only in the compiled file where it is written.
// cpp file static X const x; // not accessible to other files static int f(int x) // not accessible to other files { return x * 42; }
Is declaring our object static
in the header an alternative then? Not really, as it leaves a part of the problem unsolved:
If we declared our object static
like this in the header file:
// header.h static X const x;
Then each file that #include
it would have its own object x
. There wouldn’t be a violation of the ODR, because there would be as many x
as compiled files that #include
the header, but each one would only have its own definition.
The problem with static
is the fact that there would be several x
instead of one. It’s a shame to execute the constructor and destructor of X
for each instance, and in the (unlikely, unrecommended) case of the constructor relying on global variables, each instance of the “constant” x
could be defined differently and have its own value.
Note that putting x
in an anonymous namespace would have the same effect as declaring it static
.
The cart before the horse
To understand how to declare global constants in C++, you need to have some understanding of how a C++ program in built: preprocessing, compiling, linking.
At one point you need to master the build process of C++ anyway, but it may seem a bit surprising that such a basic feature as global constants have this pre-requisite. Anyway, that’s how it is, and it’s a good thing to master both anyway!
You will also like
- The “Extract Interface” refactoring, at compile time
- FSeam: A mocking framework that doesn’t require to change code
Share this post!