#043 – Three ways to share constants across translation units

Some applications make use of immutable, shared constants such as pi or Boltzmann’s constant.

The question arises on how to use them across multiple translation units without violating the ODR.

C++ has offered three answers over the years, and only one of them is the right default in modern code.

Three things to take away:

  • constexpr constants in a header give every translation unit its own copy.
  • extern const shares one definition but loses constant-expression use at the call site.
  • inline constexpr (C++17) gives you both: one definition, full constexpr semantics

1st Solution – Header-Defined constexpr Constants

  1. Create a header file to store constants
#ifndef CONSTANTS_H
#define CONSTANTS_H

// For extra organization, store them in a namespace
namespace constants
{
    constexpr double pi { 3.14159 };
    constexpr double boltzmann { 1.380649e-23 }; // Joules per Kelvin (J/K)
    // Insert other constants
}
#endif

2. Import the header file for any files that require the use of the constants.

#include <iostream>
#include "constants.h"

int main() 
{
    // Temperature in Kelvin
    double temperature { 298.15 }; 
    
		// Invoke the constant using "::"
    double kineticEnergy = 1.5 * constants::boltzmann * temperature;
}

    This is suitable for smaller programs. However, since the constants are constexpr they have internal linkage. If we need to import CONSTANTS into 22 files, each translation units receives it’s own unique copy. The result is that the constants are copied 22 times.

    This is problematic for two reasons:

    1. If the constant is changed, the compiler has to recompile every single file that imports CONSTANTS
    2. If the constants are large (say a lookup table or a long string literal), the compiler cannot optimize away the variable. This leads to significant memory usage.

    2nd solution – Applying extern

    To remove the costs of duplication, we can apply extern to the constants.

    This gives the variables external linkage, meaning each file doesn’t have to instantiate their own version.

    To do this, we have a .h file that contains the variable definitions and a .cpp for the implementation. It’s usage in main() remains the same.

    // constants.h
    #ifndef CONSTANTS_H
    #define CONSTANTS_H
    
    // For extra organization, store them in a namespace
    namespace constants
    {
        extern const double pi;
        extern const double boltzmann; // Joules per Kelvin (J/K)
        // Insert other constants
    }
    #endif
    
    // constants.cpp
    #ifndef CONSTANTS_H
    #define CONSTANTS_H
    
    // For extra organization, store them in a namespace
    namespace constants
    {
        extern constexpr double pi { 3.14159 };
        extern constexpr double boltzmann { 1.380649e-23 }; // Joules per Kelvin (J/K)
        // Insert other constants
    }
    #endif

    However, this strategy comes with downsides

    1. Every other file that uses the constants relies on the forward declaration. This isn’t constexpr thus if the constants are used outside of constants.cpp it cannot be apart of a constant expression
    2. We require 2 separate files

    3rd solution – inline constexpr variables (C++17)

    #042 – inline is no longer about performance discussed the use of inline to functions. As of C++17, they can be applied to variables.

    An inline variable may be defined in a header and included from any number of translation units without violating the ODR — the linker accepts the duplicate definitions and merges them into a single entity

    // constants.h
    #ifndef CONSTANTS_H
    #define CONSTANTS_H
    
    // For extra organization, store them in a namespace
    namespace constants
    {
        inline constexpr double pi;
        inline constexpr double boltzmann; // Joules per Kelvin (J/K)
        // Insert other constants
    }
    #endif

    However, if a variable changes in constants.h, this triggers a recompilation for every file that imports it.

    In Conclusion

    If you need global constants and your compiler is C++17 capable, prefer defining inline constexpr global variables in a header file.