Jonathan Boccara's blog

What Every C++ Developer Should Know to (Correctly) Define Global Constants

Published July 23, 2019 - 0 Comments

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 #included in several .cpp files?

global constant C++

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;

global constant inline C++

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 #includeing 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:

global constant extern

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:

global constant values C++

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

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin