inline Variables (C++17)
"C++ inline variables: solving ODR for header-defined globals, inline static data members, constexpr implies inline, variable templates, and practical patterns."
inline variablesince C++17A variable declared inline may be defined in multiple translation units simultaneously; the linker collapses all identical definitions into a single object, guaranteeing a unique address and a single instance across the entire program.
Overview
Before C++17, placing a variable definition in a header caused an ODR (One Definition Rule) violation the moment that header was included in more than one translation unit. The only escapes were extern declarations paired with a single out-of-line definition, or static β which silently gives each translation unit its own independent copy, breaking address identity and wasting space for large constants.
C++17 extended the semantics of inline to variables. The guarantee is the same as for inline functions: the program behaves as if there is exactly one object, regardless of how many translation units include its definition. If you take the address of an inline variable in two different translation units, you get the same pointer value.
Two corollaries fall out of this immediately:
constexprat namespace scope is implicitlyinlinein C++17. Anyconstexprvariable in a header is already safe to include everywhere; theinlinekeyword is redundant but sometimes written explicitly for clarity.inline staticdata members no longer need an out-of-class definition. This eliminates the split between.hppdeclaration and.cppdefinition that plagued pre-C++17 class design.
Syntax
// Namespace-scope inline variable
inline int global_counter = 0; // C++17
inline constexpr double pi = 3.14159265358979323; // C++17
// constexpr at namespace scope is implicitly inline (C++17)
constexpr double tau = 2.0 * pi; // implicitly inline, C++17
// Inline static data member β defined inside the class
struct Config {
inline static int instance_count = 0; // C++17
inline static constexpr std::size_t max_len = 4096; // C++17
};
// Variable template with inline constexpr (standard library pattern)
template<typename T>
inline constexpr bool is_trivially_relocatable_v = /* ... */; // C++17Examples
Header-only library constants
The canonical pattern for constants in a header-only library:
// mylib/config.hpp
#pragma once
#include <string_view>
#include <cstddef>
namespace mylib {
inline constexpr std::string_view version = "2.1.0"; // C++17
inline constexpr std::size_t max_workers = 32;
inline constexpr bool debug_mode = false;
}Every translation unit that includes this header shares the same version object. Contrast with static constexpr, which would give each translation unit its own copy β a difference that is invisible for scalars but becomes linker bloat for non-trivial constants like large lookup tables.
Inline static data members
Before C++17, a non-const (or non-integral) static data member required a separate out-of-class definition in exactly one .cpp file. With inline static, the definition lives in the class:
// connection_pool.hpp β fully self-contained, C++17
#pragma once
#include <atomic>
#include <cstddef>
class ConnectionPool {
public:
explicit ConnectionPool(std::size_t capacity);
inline static std::atomic<int> active_count{0}; // shared across all TUs, C++17
inline static constexpr std::size_t max_capacity = 128; // C++17
ConnectionPool(const ConnectionPool&) = delete;
ConnectionPool& operator=(const ConnectionPool&) = delete;
};Without inline, active_count would need a companion line in connection_pool.cpp:
// pre-C++17 .cpp required:
std::atomic<int> ConnectionPool::active_count{0};static constexpr vs inline constexpr β the bloat distinction
This distinction matters for non-scalar constants in headers:
// BAD for large data in headers β each TU gets its own copy
static constexpr std::array<int, 1024> lookup_table = make_table(); // static = per-TU copy
// GOOD β linker merges into one shared object
inline constexpr std::array<int, 1024> lookup_table = make_table(); // C++17For integer and floating-point scalars the compiler typically inlines the value at every use site and no actual object is emitted, so the distinction is academic. For compound types (arrays, structs), static constexpr in a widely-included header genuinely replicates the data.
Variable templates
The standard library's type-trait helper variables β the _v suffix family β are all inline constexpr variable templates. Writing your own follows the same pattern:
// Trait detection helper β matches std::is_arithmetic_v style
template<typename T>
inline constexpr bool is_trivially_relocatable_v = // C++17
std::is_trivially_move_constructible_v<T> &&
std::is_trivially_destructible_v<T>;
// Usage: no <> noise, reads like a value
static_assert(is_trivially_relocatable_v<int>);
static_assert(!is_trivially_relocatable_v<std::string>);Without inline, each instantiation of this variable template in different translation units would be an ODR violation. inline is essentially mandatory on variable templates defined in headers.
std::numbers (C++20 extension)
C++20's <numbers> header exposes mathematical constants as inline constexpr variable templates:
#include <numbers> // C++20
// std::numbers::pi_v<double> is: template<> inline constexpr double pi_v<double> = ...;
// std::numbers::pi is: inline constexpr double pi = pi_v<double>;
double circumference(double r) {
return 2.0 * std::numbers::pi * r; // C++20
}This is the same mechanism β variable template + inline constexpr β available since C++17 and codified in the standard library in C++20.
Best Practices
Prefer inline constexpr over static constexpr for globals in headers. static gives per-TU copies; inline gives one shared object. Use static constexpr only inside function bodies or anonymous namespaces where you explicitly want TU-local behavior.
Always mark variable templates in headers as inline. A variable template in a header without inline is almost certainly an ODR violation waiting to happen.
Use inline static for class-level shared state. Instance counters, shared configuration, and other class-wide mutable state that previously required a .cpp file are the primary use case.
Keep mutable inline globals rare. An inline int counter = 0 accessible from every translation unit is global mutable state β all the usual concurrency hazards apply. If mutation is required, reach for inline static std::atomic<int> or a proper synchronization mechanism.
Common Pitfalls
static in an anonymous namespace is not the same as inline. static (or an anonymous namespace) intentionally gives each TU its own copy and a unique internal-linkage symbol. inline gives external linkage and one shared symbol. Mixing them up causes silent correctness bugs with mutable globals.
namespace {
int counter = 0; // internal linkage β each TU has its own counter
}
inline int counter = 0; // C++17 β one counter shared across all TUsMutable non-constexpr inline variables with dynamic initializers have unpredictable initialization order relative to other globals in the same TU and across TUs (the static initialization order fiasco still applies). Restrict inline globals with dynamic initializers to cases where initialization order does not matter, or prefer constexpr / constinit (C++20).
// Dangerous: initialization order vs other globals is unspecified
inline std::string app_name = compute_name(); // dynamic init β fragile
// Safer: constexpr guarantees compile-time initialization
inline constexpr std::string_view app_name = "myapp"; // C++17constinit (C++20) enforces constant initialization without implying inline. If you want to guarantee a global is zero-initialized or constant-initialized but do not want the variable shared across TUs, use constinit without inline. If you want both guarantees, combine them: inline constinit.
Forgetting inline on a variable template definition. A variable template defined in a header without inline compiles cleanly but produces an ODR violation when the same specialization is instantiated in more than one translation unit:
// bug.hpp β missing inline, ODR violation if included in multiple .cpps
template<typename T>
constexpr bool always_false = false; // β should be inline constexpr
// fix.hpp
template<typename T>
inline constexpr bool always_false = false; // C++17 β safe