Sharing Global Constants Across Multiple Files (Using Inline Variables)

In many applications, certain constants need to be accessible throughout your codebase. These might include mathematical constants (like Euler's number or the golden ratio), physical constants (like the speed of light), or application-specific configuration values (like maximum connection limits or default timeout durations).

Defining these constants repeatedly wherever needed violates the "Don't Repeat Yourself" principle. Instead, define them centrally and reference them throughout your program. This way, updates require changes in only one location, automatically propagating everywhere the constant is used.

This lesson covers the most effective approaches for sharing constants across files.

Global Constants as Internal Variables

Before C++17, the standard approach was:

  1. Create a header file for your constants.
  2. Define a namespace within that header.
  3. Place all constants inside the namespace as constexpr values.
  4. Include the header wherever needed.

Example:

config.h:

#pragma once

namespace config
{
    constexpr int maxConnections{ 150 };
    constexpr double timeoutSeconds{ 30.0 };
    constexpr double serverVersion{ 2.4 };
}

Access these constants using the scope resolution operator with the namespace:

main.cpp:

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

int main()
{
    std::cout << "Enter number of connections: ";
    int connections{};
    std::cin >> connections;

    std::cout << "Connections allowed: " << (connections <= config::maxConnections) << '\n';

    return 0;
}

When this header is included, each constant is copied into the including file at the inclusion point. These become global variables within that file's scope.

Because const globals have internal linkage, each source file gets its own independent copy invisible to the linker. In most cases, since these are constexpr, the compiler optimizes them away entirely.

This approach is simple and works well for smaller programs, but has drawbacks. When config.h is included in twenty files, each constant is duplicated twenty times. Header guards prevent multiple inclusions within a single file but don't prevent inclusion into multiple different files. This creates two challenges:

  1. Changing any constant requires recompiling every file including the constants header, causing potentially lengthy rebuild times in larger projects.
  2. If constants are large and cannot be optimized away, this wastes memory.

Advantages:

  • Works before C++17.
  • Constants usable in constant expressions throughout any translation unit including them.

Downsides:

  • Modifying the header requires recompiling all including files.
  • Each translation unit gets its own copy of every variable.

Global Constants as External Variables

If you're frequently changing values or adding constants, the previous solution becomes problematic during development.

One alternative uses external variables, allowing a single variable instance shared across all files. Define constants in a source file (ensuring single-instance definitions) and provide forward declarations in the header (included by other files).

config.cpp:

#include "config.h"

namespace config
{
    extern constexpr int maxConnections{ 150 };
    extern constexpr double timeoutSeconds{ 30.0 };
    extern constexpr double serverVersion{ 2.4 };
}

config.h:

#pragma once

namespace config
{
    extern const int maxConnections;
    extern const double timeoutSeconds;
    extern const double serverVersion;
}

Usage remains identical:

main.cpp:

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

int main()
{
    std::cout << "Enter number of connections: ";
    int connections{};
    std::cin >> connections;

    std::cout << "Connections allowed: " << (connections <= config::maxConnections) << '\n';

    return 0;
}

Now constants are instantiated once (in config.cpp), not in every file including config.h. All uses link to the single instance in config.cpp. Changes to config.cpp require recompiling only config.cpp.

However, this has downsides. Only the definitions in config.cpp are constexpr—forward declarations cannot be constexpr and must be resolved by the linker. Outside config.cpp, these constants cannot be used in constant expressions. The compiler sees only the forward declaration, not a constexpr definition. Additionally, since constant expressions optimize better than runtime expressions, the compiler cannot optimize these as effectively.

For variables to be usable in compile-time contexts like array sizes, the compiler must see the variable definition, not just a forward declaration.

The compiler processes each source file independently, seeing only variable definitions in the current file (including headers). Definitions in config.cpp remain invisible when compiling main.cpp. Therefore, constexpr variables cannot be split between header and source files—they must be defined in the header.

Given these limitations, prefer defining constants in headers (using either the previous approach or the next one). If frequently changing constant values causes long recompilation times, temporarily move just those constants into a source file using this method.

Advantages:

  • Works before C++17.
  • Only one copy of each variable exists.
  • Requires recompiling only one file when constant values change.

Disadvantages:

  • Forward declarations and definitions exist separately and must stay synchronized.
  • Variables unusable in constant expressions outside their definition file.

Global Constants as Inline Variables (C++17)

In the previous lesson, we covered inline variables: variables allowed to have multiple definitions, provided those definitions are identical. Making constexpr variables inline allows defining them in a header and including them into any source file needing them. This avoids both ODR violations and variable duplication downsides.

Constexpr functions are implicitly inline, but constexpr variables are not. For an inline constexpr variable, you must explicitly mark it inline.

Inline variables have external linkage by default, making them visible to the linker. This is necessary for the linker to deduplicate definitions.

Non-inline constexpr variables have internal linkage. When included in multiple translation units, each gets its own copy—not an ODR violation because they're not exposed to the linker.

config.h:

#pragma once

namespace config
{
    inline constexpr int maxConnections{ 150 };
    inline constexpr double timeoutSeconds{ 30.0 };
    inline constexpr double serverVersion{ 2.4 };
}

main.cpp:

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

int main()
{
    std::cout << "Enter number of connections: ";
    int connections{};
    std::cin >> connections;

    std::cout << "Connections allowed: " << (connections <= config::maxConnections) << '\n';

    return 0;
}

Including config.h in multiple files creates only a single instance of each variable, shared across all files.

This approach retains one downside: changing the header requires recompiling all files including it.

Advantages:

  • Constants usable in constant expressions throughout any translation unit including them.
  • Only one copy of each variable exists.

Downsides:

  • Requires C++17 or later.
  • Modifying the header requires recompiling all including files.

If you need global constants and your compiler supports C++17, prefer defining inline constexpr global variables in headers.

For constexpr strings, use std::string_view.

We summarize scope, duration, and linkage of various variable types in an upcoming lesson.

Summary

Sharing constants challenge: Many applications need constants accessible throughout the codebase. Defining them in multiple places violates DRY principles and creates maintenance issues.

Method 1 - Internal variables (pre-C++17): Define constexpr constants in a header within a namespace. Each translation unit gets its own copy with internal linkage. Simple but causes recompilation of all including files when values change and potentially wastes memory.

Method 2 - External variables: Define constants in a source file with extern constexpr, forward declare in header as extern const. Only one instance exists, less recompilation. However, constants cannot be used in constant expressions outside their definition file.

Method 3 - Inline variables (C++17, preferred): Use inline constexpr in headers. Single instance shared across files, usable in constant expressions everywhere, but requires C++17+ and causes recompilation when header changes.

Constexpr vs inline: Constexpr functions are implicitly inline, but constexpr variables are not. You must explicitly mark constexpr variables as inline to share them across files.

Linkage difference: Non-inline constexpr variables have internal linkage. Inline constexpr variables have external linkage, allowing the linker to deduplicate definitions.

For modern C++17+ projects, inline constexpr variables in headers provide the best solution for sharing global constants: they're usable in constant expressions, don't duplicate memory, and maintain a single source of truth for constant values.