ядро языка — синтаксисcore language — syntax

inline-переменныеinline variables

inline теперь применим не только к функциям, но и к переменным — можно определить static-член класса или глобальную переменную прямо в заголовке без нарушения ODR при подключении в несколько единиц трансляции.inline now applies to variables too, not just functions — a class's static member or a global variable can be defined right in a header without violating the ODR when the header is included in multiple translation units.

header-only библиотекиheader-only libraries частая путаница со смыслом inlinecommonly confused with inlining

Что этоWhat it is

До C++17 ключевое слово inline можно было применять только к функциям. C++17 разрешает пометить inline и переменную — тогда её ОПРЕДЕЛЕНИЕ (не только объявление) можно повторить дословно в нескольких единицах трансляции, и линкер объединит все повторы в один-единственный объект в итоговой программе, а не сообщит об ошибке "multiple definition".Before C++17, the inline keyword could only be applied to functions. C++17 allows marking a variable inline too — its DEFINITION (not just a declaration) can then be repeated verbatim across multiple translation units, and the linker will merge all the repeats into a single object in the final program, instead of reporting a "multiple definition" error.

до C++17 — определение только в одном .cppbefore C++17 — definition in exactly one .cpp
// widget.hpp// widget.hpp
struct Widget {
    static int counter;  // только объявление// declaration only
};

// widget.cpp — обязателен, иначе linker error// widget.cpp — mandatory, otherwise a linker error
int Widget::counter = 0;
C++17 — inline-переменная прямо в заголовкеC++17 — inline variable right in the header
// widget.hpp — .cpp больше не нужен// widget.hpp — no .cpp needed anymore
struct Widget {
    static inline int counter = 0;  // определение и инициализация тут же// definition and initialization right here
};

Зачем ввелиWhy it was introduced

Требование выносить определение static-члена в отдельный .cpp было историческим следствием того, как ODR (One Definition Rule) трактовал переменные, и плохо сочеталось с растущей популярностью header-only кода.The requirement to move a static member's definition into a separate .cpp file was a historical consequence of how the ODR (One Definition Rule) treated variables, and it sat poorly with the growing popularity of header-only code.

01

Header-only библиотеки не могли содержать неконстантные static-членыHeader-only libraries couldn't contain non-const static members

Библиотека, состоящая только из заголовков (без отдельных .cpp для сборки), физически не могла разместить определение static int counter; нигде — единственным легальным местом был .cpp-файл, которого в header-only библиотеке по определению нет. Обходные пути (static-функция с локальной static-переменной внутри) работали, но были неочевидны и многословны для такой простой задачи.A library consisting only of headers (with no separate .cpp files to build) physically had nowhere to put the definition of static int counter; — the only legal place was a .cpp file, which a header-only library by definition doesn't have. Workarounds (a static function wrapping a local static variable) worked, but were non-obvious and verbose for such a simple need.

02

Асимметрия между inline-функциями и переменными была нелогичнойThe asymmetry between inline functions and variables made no sense

Функции уже давно можно было определять прямо в заголовке через inline, и это стандартный способ разместить реализацию в header-only коде. То, что то же самое было невозможно для переменных, выглядело искусственным ограничением, а не осознанным решением дизайна языка.Functions could already be defined right in a header via inline for a long time, and that's the standard way to place an implementation in header-only code. The fact that the same wasn't possible for variables looked like an arbitrary limitation, not a deliberate language design choice.

03

constexpr static-члены были частичным, но неполным решениемconstexpr static members were a partial but incomplete solution

static constexpr члены уже можно было инициализировать в классе, но это работало только для constexpr-совместимых типов и значений, известных на этапе компиляции. Изменяемые static-переменные (счётчики, кэши, синглтоны) оставались без решения до inline-переменных.static constexpr members could already be initialized in-class, but that only worked for constexpr-compatible types and compile-time-known values. Mutable static variables (counters, caches, singletons) had no solution until inline variables.

Как это работает под капотомHow it works under the hood

Смысл слова inline здесь — тот же, что и у inline-функций: это НЕ подсказка компилятору подставить код вместо вызова (это заблуждение, унаследованное из мира функций, где оптимизатор давно принимает решение об инлайнинге сам, независимо от ключевого слова). Смысл — "определение может законно повторяться в нескольких единицах трансляции, и линкер обязан схлопнуть все копии в один объект".The meaning of inline here is the same as for inline functions: it does NOT mean a hint to the compiler to substitute code in place of a call (that's a misconception inherited from the world of functions, where the optimizer has long made inlining decisions on its own, independent of the keyword). The meaning is "this definition may legally repeat across multiple translation units, and the linker must collapse all copies into a single object."

// widget.hpp, включается и в a.cpp, и в b.cpp// widget.hpp, included in both a.cpp and b.cpp
struct Widget {
    static inline int counter = 0;
};

// после препроцессора и a.cpp, и b.cpp содержат идентичное определение counter —// after preprocessing, both a.cpp and b.cpp contain an identical counter definition —
// без inline это была бы ошибка компоновки "multiple definition of Widget::counter";// without inline this would be a "multiple definition of Widget::counter" link error;
// с inline линкер видит два идентичных слабых символа и оставляет один// with inline the linker sees two identical weak symbols and keeps just one

// глобальная переменная вне класса — та же идея:// a global variable outside a class — same idea:
inline std::vector<int> g_registry;  // легально в заголовке, единственный объект на программу// legal in a header, a single object across the program

Механизм реализуется через тот же линкерный приём, что и обычные inline-функции — так называемые "слабые" (weak/COMDAT) символы: компилятор помечает символ как допускающий дублирование, и линкер на этапе финальной сборки программы выбирает одну из идентичных копий, отбрасывая остальные. Гарантируется также, что объект будет один и тот же во всех единицах трансляции — можно безопасно брать его адрес и сравнивать указатели между разными .cpp-файлами.The mechanism uses the same linker trick as ordinary inline functions — so-called "weak" (weak/COMDAT) symbols: the compiler marks the symbol as allowed to duplicate, and the linker, during the program's final link step, picks one of the identical copies and discards the rest. It's also guaranteed that the object will be the very same one across all translation units — its address can safely be taken and pointers compared across different .cpp files.

ГраблиGotchas

inline у переменной ≠ inline у функции по смыслу оптимизацииinline on a variable ≠ inline on a function in terms of optimization

Название вводит в заблуждение: инженеры, привыкшие думать про inline-функции как "подсказка встроить код", по инерции ищут аналогичный оптимизационный эффект у inline-переменных. Такого эффекта нет и не может быть — переменная не "встраивается" никуда, слово решает исключительно вопрос ODR и компоновки.The name is misleading: engineers used to thinking of inline functions as "a hint to embed the code" instinctively look for a similar optimization effect with inline variables. No such effect exists or can exist — a variable isn't "embedded" anywhere; the keyword solely resolves an ODR and linking question.

static-члены класса, объявленные внутри тела класса, — уже неявно inlinestatic class members declared inside the class body are already implicitly inline

static inline int counter = 0; внутри класса можно писать и без явного inline — начиная с C++17 static-члены, инициализированные прямо в определении класса, автоматически становятся inline. Явное слово чаще нужно для inline-переменных ВНЕ класса (глобальных/namespace-scope) или для читаемости кода, подчёркивающей намерение.static inline int counter = 0; inside a class can be written without the explicit inline too — starting with C++17, static members initialized right in the class definition become implicitly inline. The explicit keyword is more often needed for inline variables OUTSIDE a class (global/namespace-scope) or for readability that makes the intent explicit.

Порядок инициализации между единицами трансляции всё ещё не гарантированInitialization order across translation units is still not guaranteed

inline-переменные решают проблему ODR, но не решают классическую проблему "static initialization order fiasco" — если одна inline-переменная в своей инициализации зависит от другой inline-переменной из другой единицы трансляции, порядок их инициализации между TU по-прежнему не определён языком.Inline variables solve the ODR problem, but they don't solve the classic "static initialization order fiasco" — if one inline variable's initializer depends on another inline variable from a different translation unit, the order in which they're initialized across TUs is still unspecified by the language.