Skip to content
C++
Language
since C++17
Basic

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++17

A 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:

  • constexpr at namespace scope is implicitly inline in C++17. Any constexpr variable in a header is already safe to include everywhere; the inline keyword is redundant but sometimes written explicitly for clarity.
  • inline static data members no longer need an out-of-class definition. This eliminates the split between .hpp declaration and .cpp definition that plagued pre-C++17 class design.

Syntax

cpp
// 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++17

Examples

Header-only library constants

The canonical pattern for constants in a header-only library:

cpp
// 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:

cpp
// 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:

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:

cpp
// 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++17

For 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:

cpp
// 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:

cpp
#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.

cpp
namespace {
    int counter = 0;   // internal linkage β€” each TU has its own counter
}
inline int counter = 0;  // C++17 β€” one counter shared across all TUs

Mutable 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).

cpp
// 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++17

constinit (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:

cpp
// 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

The _v Helper Pattern: How the Standard Library Uses inline constexpr

C++17 added _v suffix variable templates to <type_traits> to replace the verbose ::value member access. Every one of them is defined as an inline constexpr variable template. For example, std::is_same_v is defined exactly as:

cpp
// From <type_traits> β€” the actual standard library definition
template<class T, class U>
inline constexpr bool is_same_v = std::is_same<T, U>::value;  // C++17

The inline is not optional here: without it, every translation unit that instantiates is_same_v<int, float> for the first time would create its own definition of that specialization, causing an ODR violation at link time.

The ergonomic improvement over the pre-C++17 form is significant:

cpp
// C++14 and earlier β€” member access through :: requires typename in dependent contexts
static_assert(std::is_same<int, int>::value);
static_assert(std::is_integral<long>::value);
static_assert(std::is_convertible<float, double>::value);

// C++17 β€” _v helpers read like boolean expressions
static_assert(std::is_same_v<int, int>);
static_assert(std::is_integral_v<long>);
static_assert(std::is_convertible_v<float, double>);

The same pattern applies to the _t type-alias templates (e.g. std::remove_const_t<T>), except those use using aliases rather than variable templates and do not require inline.

When writing your own type trait helpers, follow the same convention:

cpp
// mytraits.hpp β€” safe to include in multiple TUs
#pragma once
#include <type_traits>

// Primary trait as a struct (C++11 style)
template<typename T>
struct is_pod_like : std::conjunction<
    std::is_trivially_copyable<T>,
    std::is_standard_layout<T>
> {};

// Inline constexpr helper β€” the _v idiom (C++17)
template<typename T>
inline constexpr bool is_pod_like_v = is_pod_like<T>::value;

Initialization Order Across Translation Units

Because an inline variable has a single definition shared across all TUs, its dynamic initialization happens exactly once β€” unlike a static variable in a header, which would be a separate object in every TU and thus initialized once per TU.

The C++17 standard (via proposal P0386) specifies partial ordering for inline variable initialization: if an inline variable V is defined before some other variable W in every TU where W appears, then V is guaranteed to be initialized before W.

cpp
// header.h
int compute_base();
inline int base_value = compute_base();   // initialized exactly once, C++17

// file1.cpp β€” includes header.h
#include "header.h"
int derived1 = base_value + 10;          // partial order: base_value initializes first
                                         // within every TU that defines derived1

// file2.cpp β€” includes header.h
#include "header.h"
int derived2 = base_value * 2;           // same guarantee: base_value comes first

The partial ordering guarantee means derived1 and derived2 can each safely read base_value because base_value is defined before them in every TU in which they appear. However, the relative ordering of derived1 vs derived2 with respect to each other is still unspecified β€” they live in different TUs and do not depend on each other.

This is a stricter guarantee than for non-inline globals, which have unordered initialization relative to globals in other TUs. The tradeoff: constexpr inline variables avoid the question entirely because they have constant (compile-time) initialization with no runtime dependency.

Key rules to remember:

  • An inline variable with dynamic initialization is initialized once per program, not once per TU.
  • The partial ordering rule only applies when V is textually earlier than W in every TU containing W.
  • Between TUs that do not share an ordering relationship, initialization order is still unspecified β€” the static initialization order fiasco still applies.
  • When in doubt, prefer inline constexpr (constant initialization, no ordering concerns) over inline with a dynamic initializer.

Pre-C++17 Workarounds for Static Data Members

Before C++17, putting a non-trivial static data member in a header required one of several unsatisfying patterns. Understanding the workarounds clarifies why inline static is such a clean solution.

Workaround 1: extern + separate .cpp definition. The classic approach β€” declare in the header, define in exactly one .cpp:

cpp
// widget.hpp (pre-C++17)
class Widget {
public:
    static int instance_count;   // declaration only
};

// widget.cpp (pre-C++17) β€” must exist; exactly one .cpp must define this
int Widget::instance_count = 0;

Problem: forces a .cpp file, breaks header-only libraries.

Workaround 2: static local in an inline function. Used in header-only libraries to get a single shared object:

cpp
// widget.hpp (pre-C++17 header-only workaround)
class Widget {
public:
    static int& instance_count() {
        static int count = 0;   // initialized on first call, shared via inline function rules
        return count;
    }
};

// Usage is syntactically noisy
Widget::instance_count()++;

Problem: call-site syntax is awkward; callers must use (). Not usable for compile-time constants.

Workaround 3: enum hack for integral constants. Integral constants only, but avoids linkage issues:

cpp
// widget.hpp (pre-C++17 integer constant workaround)
class Widget {
    enum : std::size_t { max_count = 1024 };  // no linkage at all; value-only
};

Problem: non-portable for non-integer types; cannot take the address of an enum enumerator.

C++17 solution β€” inline static eliminates all workarounds:

cpp
// widget.hpp (C++17 β€” fully self-contained)
#pragma once

class Widget {
public:
    inline static int instance_count = 0;              // mutable shared counter
    inline static constexpr std::size_t max_count = 1024; // compile-time constant
};

No companion .cpp. One declaration that is also the definition. The linker merges all copies into a single object with external linkage. instance_count has the same address in every TU.

inline constexpr for String Constants

String literals in headers have an ODR trap that inline constexpr std::string_view neatly sidesteps. A bare const char* or even a constexpr char[] at namespace scope in a header gives each translation unit its own copy with its own address. Code that compares pointers β€” rather than string contents β€” will silently disagree across TUs.

std::string_view is a trivial, zero-overhead view type whose data() pointer identifies the underlying string storage. When the backing string_view is inline constexpr, the linker collapses all TU copies into one object, so pointer identity is preserved:

cpp
// C++17 β€” single canonical object, consistent address across all TUs
#pragma once
#include <string_view>

namespace mylib {
    // BAD: each TU gets a different char array; pointer comparison unreliable
    constexpr char    bad_name[] = "MyLib";       // static linkage per TU

    // GOOD: one object, one address, pointer comparison works
    inline constexpr std::string_view lib_name   = "MyLib";    // C++17
    inline constexpr std::string_view lib_version = "3.0.0";   // C++17
}

std::string_view is trivially constructible from a string literal at compile time, so this compiles to nothing at runtime β€” the view is resolved entirely by the linker. Avoid inline constexpr std::string for the same purpose: std::string has dynamic allocation semantics and the constexpr std::string feature (C++20) still performs heap allocation in non-consteval contexts on many implementations.

cpp
// C++20 β€” constexpr std::string compiles but may allocate at runtime;
// prefer string_view for header constants
inline constexpr std::string_view error_prefix = "[error] ";  // C++17, zero cost
// inline constexpr std::string  error_prefix = "[error] ";  // C++20, may heap-alloc

For multi-word constant tables, combine inline constexpr with std::array<std::string_view, N> β€” all still resolved at compile time with a single shared object:

cpp
// C++17 β€” table of string_view constants, one shared object
#include <array>
#include <string_view>

inline constexpr std::array<std::string_view, 3> log_levels = {
    "DEBUG", "INFO", "ERROR"
};

inline Variables in Template Classes

Before C++17, static data members of class templates required an out-of-class definition that itself had to be a template β€” a uniquely tedious pattern that inline static abolished. The problem is that a class template is not itself a class; each instantiation is a separate class, and its static members are therefore separate variables, each needing a definition.

Consider the C++14 approach for a pool allocator that tracks allocation counts per type:

cpp
// C++14 β€” static data member of a class template
// allocator.hpp
template<typename T>
class PoolAllocator {
public:
    static std::size_t allocation_count;  // declaration only
};

// allocator.cpp β€” must also be a template definition, in a header or explicit instantiation
template<typename T>
std::size_t PoolAllocator<T>::allocation_count = 0;  // out-of-class template definition

Including allocator.cpp from multiple translation units causes ODR violations; placing the out-of-class definition back in the header compiles but creates one copy per TU (under the old static semantics). The standard workaround was to hide the definition in the header and rely on the implicit one-definition exception for template specializations β€” but this only worked for constexpr integral members.

C++17 resolves all of this cleanly:

cpp
// C++17 β€” inline static in a class template, self-contained in the header
#pragma once
#include <cstddef>

template<typename T>
class PoolAllocator {
public:
    inline static std::size_t allocation_count = 0;           // C++17
    inline static constexpr std::size_t alignment = alignof(T); // C++17

    static void reset() { allocation_count = 0; }
};

// Each instantiation PoolAllocator<Foo>, PoolAllocator<Bar>, etc.
// gets its own independent inline static, correctly collapsed by the linker.
static_assert(PoolAllocator<int>::alignment == alignof(int));

Each specialization PoolAllocator<T> has its own allocation_count instance β€” that is the expected per-type behavior. inline static ensures that within a given specialization, all TUs see the same object.

Interaction with [[maybe_unused]] and [[deprecated]]

Attributes on inline constants follow the same syntax as any other variable declaration, and combining them with inline constexpr produces exactly the expected diagnostics. This matters most in library headers where constants are published for users but some may be superseded across versions.

[[maybe_unused]] silences the "unused variable" warning that some compilers emit for inline constexpr values that happen not to be referenced in a given translation unit:

cpp
// C++17 β€” attribute suppresses unused-variable warnings on opt-in constants
#pragma once

[[maybe_unused]] inline constexpr int debug_verbosity = 0;   // C++17
[[maybe_unused]] inline constexpr bool enable_profiling = false; // C++17

[[deprecated]] works with the optional message argument and applies naturally to inline constants that have been superseded:

cpp
// C++17 β€” deprecated inline constant with migration guidance
#pragma once
#include <cstddef>

[[deprecated("use buffer_capacity_v2 β€” v1 is not cache-line aligned")]]
inline constexpr std::size_t buffer_capacity = 1024;          // C++17

inline constexpr std::size_t buffer_capacity_v2 = 4096;       // C++17

Any translation unit that references buffer_capacity will receive the deprecation diagnostic at its call site. The attribute can be combined with other attributes in a single [[...]] block:

cpp
// C++17 β€” multiple attributes on one inline constexpr
[[deprecated("prefer pi_v from <numbers> in C++20"), maybe_unused]]
inline constexpr double pi = 3.14159265358979323;  // C++17

Attributes do not affect linkage or the ODR guarantee β€” an attributed inline constexpr is still a single shared object. They are purely a diagnostics mechanism.

inline Variables vs constinit (C++20)

constinit and inline solve different problems, and conflating them leads to either unnecessary TU-duplication or unnecessary linkage exposure. constinit (C++20) is a guarantee annotation: it tells the compiler to reject the declaration if the initializer is not a constant expression, guarding against the static initialization order fiasco. It says nothing about linkage or whether one or many copies of the variable exist.

inline, by contrast, is a linkage and ODR directive: it ensures a single shared object with external linkage, but it does not enforce that the initializer is evaluated at compile time.

The two keywords compose orthogonally:

cpp
// C++20 β€” four combinations and what each means

// 1. inline only β€” shared object, dynamic init allowed, no compile-time enforcement
inline int a = some_runtime_function();      // C++17+

// 2. constinit only β€” TU-local (internal linkage by default), enforced compile-time init
constinit int b = 42;                        // C++20; each TU gets its own copy

// 3. inline constexpr β€” shared object, compile-time value, implicitly constinit
inline constexpr int c = 42;                 // C++17; best for header constants

// 4. inline constinit β€” shared object, enforced constant init, but value is mutable
inline constinit int d = 42;                 // C++20; like (1) but init must be constexpr
d = 99;  // legal β€” constinit does not imply const

Prefer constinit alone (without inline) when you want a TU-local mutable variable with a guaranteed compile-time initializer β€” for example, a per-TU configuration flag that should never suffer from dynamic-initialization ordering issues but also should not bleed its linkage across TU boundaries:

cpp
// C++20 β€” per-TU mutable flag with enforced constant initialization
constinit thread_local int recursion_depth = 0;  // C++20; no inline, TU-local

Prefer inline constexpr (implicitly constinit) for header constants that must be shared across TUs. Reach for inline constinit only when you need the single-shared-object guarantee of inline together with a mutable variable whose initial value must be a constant expression β€” a rare combination most commonly seen in low-level platform abstraction layers.

cpp
// C++20 β€” when inline constinit makes sense: shared mutable, guaranteed constant init
inline constinit std::atomic<int> global_epoch{0};  // C++20
// Guaranteed: initialized before any dynamic init; shared across all TUs

See Also