ядро языка — синтаксисcore language — syntax
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.
До 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.
// 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;
// widget.hpp — .cpp больше не нужен// widget.hpp — no .cpp needed anymore
struct Widget {
static inline int counter = 0; // определение и инициализация тут же// definition and initialization right here
};
Требование выносить определение 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.
Библиотека, состоящая только из заголовков (без отдельных .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.
Функции уже давно можно было определять прямо в заголовке через 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.
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.
Смысл слова 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.
Название вводит в заблуждение: инженеры, привыкшие думать про 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 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.
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.