базовые концепции ООП — константы классаbaseline OOP concepts — class constants

КонстантыConstants

Константа, принадлежащая классу, а не конкретному объекту, — это static constexpr член. До C++17 такое поле нужно было ещё и определить (не только объявить) в одном .cpp-файле отдельной строкой, если на его адрес где-то ссылались, — раздражающий ритуал, устранённый inline-переменными.A constant that belongs to the class rather than to a specific object is a static constexpr member. Before C++17, such a field also had to be defined (not just declared) in one .cpp file on a separate line, if its address was ever taken anywhere — an annoying ritual removed by inline variables.

static constexpr

ОпределениеDefinition

Класс может иметь константу, логически принадлежащую типу целиком, а не отдельному объекту, — например, максимальный размер буфера, версия формата, число измерений. Это выражается комбинацией static (одна копия на класс, не на объект) и constexpr (значение известно на этапе компиляции).A class can have a constant that logically belongs to the type as a whole, not to any specific object — say, a buffer's maximum size, a format version, a number of dimensions. This is expressed by combining static (one copy per class, not per object) and constexpr (the value is known at compile time).

class Buffer {
public:
    static constexpr size_t max_size = 4096;   // одна константа на весь класс Buffer// one constant for the whole Buffer class
private:
    std::array<char, max_size> data;   // используется прямо как compile-time значение// used directly as a compile-time value
};

// доступ снаружи — через имя класса, объект не нужен// accessed from outside via the class name, no object needed
std::cout << Buffer::max_size;

До C++17: объявление и определение — раздельноBefore C++17: declaration and definition were separate

static constexpr член внутри тела класса до C++17 был только объявлением. Если на константу где-либо брался адрес или ссылка (например, передача в функцию, принимающую const size_t&), требовалось отдельное определение вне класса, ровно в одном .cpp-файле — иначе возникала ошибка линковки "undefined reference".A static constexpr member inside a class body before C++17 was only a declaration. If the constant's address or a reference to it was ever taken anywhere (say, passing it to a function taking const size_t&), a separate definition outside the class was required, in exactly one .cpp file — otherwise, an "undefined reference" link error resulted.

// buffer.hpp// buffer.hpp
class Buffer {
public:
    static constexpr size_t max_size = 4096;   // только объявление// declaration only
};

// buffer.cpp — обязательное отдельное определение, если адрес где-то берётся// buffer.cpp — a mandatory separate definition if the address is ever taken
constexpr size_t Buffer::max_size;

Этот ритуал легко забывался, поскольку код чаще всего компилировался и без него — ошибка проявлялась только на линковке, и только если константа использовалась способом, требующим реального адреса (не встроенным `значением на месте`), что делало её трудно предсказуемой заранее.This ritual was easy to forget, since the code usually compiled fine without it — the error only surfaced at link time, and only if the constant was used in a way requiring a real address (not inlined as an immediate value), which made it hard to predict in advance.

C++17: inline-переменные устраняют ритуалC++17: inline variables remove the ritual

C++17 сделал static constexpr члены класса неявно inline — как и inline-функции, они могут быть определены одинаково в нескольких единицах трансляции (например, за счёт включения одного и того же заголовка) без нарушения One Definition Rule; линковщик схлопывает все копии в одну. Отдельное определение вне класса больше не требуется никогда.C++17 made a class's static constexpr members implicitly inline — like inline functions, they can be identically defined in multiple translation units (say, by including the same header) without violating the One Definition Rule; the linker collapses every copy into one. A separate out-of-class definition is never needed anymore.

class Buffer {
public:
    static constexpr size_t max_size = 4096;   // C++17: и объявление, и полноценное определение сразу// C++17: both declaration and full definition, immediately
};
// buffer.cpp с constexpr size_t Buffer::max_size; больше не нужен// a buffer.cpp with constexpr size_t Buffer::max_size; is no longer needed

Та же самая проблема (и то же самое решение) касается любых inline глобальных переменных вне классов — до C++17 глобальная переменная в заголовке, подключённом из нескольких .cpp, была нарушением ODR; ключевое слово inline на переменных сняло это ограничение так же, как для функций.The same problem (and the same fix) applies to any inline global variables outside classes too — before C++17, a global variable in a header included from multiple .cpp files was an ODR violation; the inline keyword on variables lifted that restriction exactly as it did for functions.

ГраблиGotchas

static const без constexpr не гарантирует compile-time значениеconst without constexpr doesn't guarantee a compile-time value

Только целочисленный/перечисляемый static const член мог быть инициализирован прямо в классе до C++11 и использоваться как compile-time константа (например, в размере массива) без явного определения — для всех остальных типов (double, свои классы) требовалось определение всегда, а сам факт "const" не означает "известно на этапе компиляции", это делает именно constexpr.Only an integral/enumeration static const member could be initialized directly in the class before C++11 and used as a compile-time constant (say, in an array size) without an explicit definition — every other type (double, custom classes) always needed a definition, and "const" alone never meant "known at compile time" — that's specifically what constexpr guarantees.

Нецелочисленный static constexpr член всё равно occupies место в бинарнике, если адрес взятA non-integral static constexpr member still occupies space in the binary if its address is taken

Даже с inline-переменными, если константа передаётся куда-то по ссылке или указателю, компилятор обязан выделить под неё реальный адрес в памяти (в .rdata/.rodata — см. страницу про PE-файл) — избежать этого позволяет использование значения только там, где достаточно инлайнинга как immediate.Even with inline variables, if the constant is ever passed somewhere by reference or pointer, the compiler must allocate a real memory address for it (in .rdata/.rodata — see the PE file page) — avoiding this means only ever using the value where inlining as an immediate is enough.

enum как альтернатива static const int — исторический трюк, больше не нуженas an alternative to static const int — a historical trick, no longer needed

До широкой поддержки in-class инициализации static const использовался трюк enum { max_size = 4096 }; внутри класса — значения enum всегда были compile-time константами без риска нарушить ODR. Сейчас это архаизм, который может встретиться в старом коде, но не имеет причин появляться в новом.Before widespread support for in-class static const initialization, the trick was enum { max_size = 4096 }; inside a class — enum values were always compile-time constants with no ODR risk. It's an archaism now, one that might turn up in old code, but has no reason to appear in new code.