ядро языка — compile-time семантикаcore language — compile-time semantics
Два новых ключевых слова, ужесточающих гарантии constexpr: consteval — функция ВСЕГДА вычисляется на этапе компиляции; constinit — переменная ВСЕГДА инициализируется статически, но остаётся изменяемой.Two new keywords that tighten constexpr's guarantees: consteval — a function ALWAYS evaluates at compile time; constinit — a variable ALWAYS initializes statically, yet stays mutable.
consteval объявляет функцию, которая ДОЛЖНА быть вычислена на этапе компиляции при каждом вызове — если вызов не может быть constant expression, это ошибка компиляции. Это отличается от constexpr-функции, которая МОЖЕТ быть вычислена как во время компиляции, так и в рантайме — выбор зависит от контекста конкретного вызова. constinit — квалификатор переменной, гарантирующий её статическую инициализацию (constant initialization, на этапе компиляции, до старта main), но НЕ делающий переменную константой: значение можно менять после инициализации, гарантия касается только момента и способа инициализации.consteval declares a function that MUST be evaluated at compile time on every call — if a call can't be a constant expression, it's a compile error. This differs from a constexpr function, which MAY be evaluated either at compile time or at runtime — the choice depends on the context of the specific call. constinit is a variable qualifier guaranteeing static initialization (constant initialization, at compile time, before main starts), but it does NOT make the variable a constant: its value can be changed after initialization — the guarantee concerns only the moment and manner of initialization.
constexpr int square(int x) { return x * x; }
constexpr int a = square(5); // вычислено на этапе компиляции// evaluated at compile time
int runtime_input();
int b = square(runtime_input());
// то же square() — но выполнится в рантайме,// the same square() — but runs at runtime,
// потому что аргумент неизвестен на этапе компиляции// because the argument isn't known at compile time
consteval int square(int x) { return x * x; }
constexpr int a = square(5); // ок, compile-time// ok, compile-time
int runtime_input();
// int b = square(runtime_input()); // ошибка компиляции —// int b = square(runtime_input()); // compile error —
// аргумент не constant expression, а consteval требует это ВСЕГДА// the argument isn't a constant expression, and consteval always requires one
Для операций, которые ДОЛЖНЫ быть compile-time всегда — генерация уникального ID типа, вычисление hash строкового литерала для последующего switch по строкам, — constexpr недостаточно строг: он даёт "может быть compile-time", а не гарантию. Ошибочный вызов с аргументом, неизвестным на этапе компиляции, молча (с точки зрения типов) переключится на рантайм-выполнение вместо ошибки компиляции.For operations that MUST be compile-time always — generating a unique type ID, hashing a string literal for a later switch-on-string — constexpr isn't strict enough: it offers "may be compile-time," not a guarantee. A mistaken call with an argument unknown at compile time will silently (from a type-system standpoint) fall back to runtime execution instead of failing to compile.
Классическая проблема C++ — порядок инициализации глобальных/статических переменных в РАЗНЫХ единицах трансляции не определён стандартом. Если инициализация одной переменной зависит от значения другой, объявленной в другом .cpp-файле, возможен доступ к ещё не инициализированной переменной — недетерминированный баг, зависящий от порядка компоновки объектных файлов. constinit гарантирует статическую инициализацию для помеченной переменной, исключая этот класс багов для неё.A classic C++ problem — the initialization order of global/static variables across DIFFERENT translation units is unspecified by the standard. If one variable's initialization depends on the value of another declared in a different .cpp file, you can end up accessing a not-yet-initialized variable — a nondeterministic bug depending on object-file link order. constinit guarantees static initialization for the marked variable, ruling out this class of bug for it.
До C++20 в языке не было способа сказать "это должно произойти на этапе компиляции, а не просто может" — ни для функций, ни для инициализации переменных. constexpr и обычные глобальные переменные оставляли компилятору свободу выбора между compile-time и рантайм там, где программист на самом деле нуждался в жёсткой гарантии.Before C++20, the language had no way to say "this must happen at compile time, not just may" — neither for functions nor for variable initialization. constexpr and ordinary global variables left the compiler free to choose between compile-time and runtime in places where the programmer actually needed a hard guarantee.
Для consteval-функции ("immediate function") компилятор обязан вычислить каждый вызов на этапе трансляции — если хотя бы один вызов невозможно свести к constant expression, вся трансляция завершается ошибкой в этой точке, а не деградирует к обычному вызову функции в рантайме. Для constinit-переменной компилятор обязан использовать constant initialization — тот же механизм, что и для constexpr-переменных на этапе инициализации, — но по завершении инициализации переменная остаётся обычным изменяемым объектом, без ограничения "неизменности" в оставшейся части программы.For a consteval function ("immediate function"), the compiler must evaluate every call at translation time — if even one call can't be reduced to a constant expression, the whole translation fails right there, instead of degrading to an ordinary runtime function call. For a constinit variable, the compiler must use constant initialization — the same mechanism constexpr variables use at their initialization step — but once initialization completes, the variable remains an ordinary mutable object, with no "immutability" restriction for the rest of the program.
// consteval — hash строкового литерала, гарантированно compile-time:// consteval — hashing a string literal, guaranteed compile-time:
consteval std::uint64_t hash_literal(const char* s, std::size_t n) {
std::uint64_t h = 1469598103934665603ull;
for (std::size_t i = 0; i < n; ++i) h = (h ^ s[i]) * 1099511628211ull;
return h;
}
switch (hash_literal("GET", 3)) { /* ... *//* ... */ }
// constinit — гарантирует момент инициализации, не иммутабельность:// constinit — guarantees the moment of initialization, not immutability:
constinit int g_counter = 0; // инициализирован статически, до main()// statically initialized, before main()
void tick() {
g_counter = g_counter + 1; // абсолютно валидно — g_counter не const// perfectly valid — g_counter isn't const
}
Отсылка к соседнему, но другому compile-time механизму: static_assert (см. ../cpp11/static-assert.html) проверяет условие на этапе компиляции и не порождает никакого кода при успехе — он не вычисляет значение, а просто утверждает факт. consteval/constinit, напротив, про то, ЧТО и КОГДА вычисляется, а не про проверку условия.A nod to a neighboring but different compile-time mechanism: static_assert (see ../cpp11/static-assert.html) checks a condition at compile time and produces no code on success — it doesn't compute a value, it just asserts a fact. consteval/constinit, by contrast, are about WHAT and WHEN gets computed, not about checking a condition.
Практический пример: диспетчеризация по строке через compile-time хеш HTTP-метода. Идея — превратить строковые литералы в целочисленные константы ещё на этапе компиляции для case-меток switch; сам хеш рантайм-строки, приходящей извне, вычисляется отдельной, обычной constexpr-функцией (той же реализацией, но без consteval), поскольку её аргумент в общем случае не constant expression:A practical example: string dispatch via a compile-time hash of an HTTP method. The idea is to turn string literals into integer constants at compile time for switch case labels; hashing the runtime string that arrives from the outside is done by a separate, ordinary constexpr function (the same implementation, just without consteval), since its argument generally isn't a constant expression:
consteval std::uint32_t fnv1a_ce(std::string_view s) { // для case-меток — только литералы// for case labels — literals only
std::uint32_t h = 2166136261u;
for (char c : s) h = (h ^ static_cast<std::uint32_t>(c)) * 16777619u;
return h;
}
constexpr std::uint32_t fnv1a(std::string_view s) { // та же логика, для рантайм-аргумента switch(...)// same logic, for the runtime switch(...) argument
std::uint32_t h = 2166136261u;
for (char c : s) h = (h ^ static_cast<std::uint32_t>(c)) * 16777619u;
return h;
}
void dispatch(std::string_view method) {
switch (fnv1a(method)) { // вычисляется в рантайме — constexpr это разрешает// evaluates at runtime — constexpr permits this
case fnv1a_ce("GET"): handle_get(); break; // case-метка — литерал, обязана быть compile-time// case label — a literal, must be compile-time
case fnv1a_ce("POST"): handle_post(); break;
default: handle_unknown();
}
}
// fnv1a_ce(method) с рантайм-method здесь был бы ошибкой компиляции —// fnv1a_ce(method) with a runtime method here would be a compile error —
// consteval именно это и не разрешает, в отличие от constexpr// that's exactly what consteval disallows, unlike constexpr
Гарантирует, что каждая case-метка — честная compile-time константа, а не случайно посчитанное в рантайме значение.Guarantees that every case label is a genuine compile-time constant, not a value accidentally computed at runtime.
consteval std::uint32_t hash_tag(std::string_view s) {
std::uint32_t h = 2166136261u;
for (char c : s) h = (h ^ static_cast<std::uint32_t>(c)) * 16777619u;
return h;
}
constexpr auto TAG_ORDER = hash_tag("order");
constexpr auto TAG_TRADE = hash_tag("trade");
Реестр, к которому другие единицы трансляции могут обращаться в своих собственных статических инициализаторах, гарантированно уже готов на момент любого такого обращения.A registry other translation units may access from their own static initializers is guaranteed to already be ready at the time of any such access.
class Logger {
public:
static constexpr int default_level() { return 2; }
};
constinit Logger g_logger{}; // готов до старта main(), никакой гонки порядка инициализации// ready before main() starts, no initialization-order race
Один и тот же вызов с рантайм-аргументом: constexpr молча уходит в рантайм-выполнение, consteval превращает это в ошибку компиляции прямо в точке вызова.The same call with a runtime argument: constexpr silently falls back to runtime execution, consteval turns it into a compile error right at the call site.
constexpr int square_ce(int x) { return x * x; }
consteval int square_cv(int x) { return x * x; }
int get_input();
int a = square_ce(get_input()); // ок — просто выполняется в рантайме// ok — simply runs at runtime
// int b = square_cv(get_input()); // ошибка компиляции: аргумент не constant expression// int b = square_cv(get_input()); // compile error: the argument isn't a constant expression
Комбинация с thread_local гарантирует, что у каждого потока инициализация его собственного экземпляра происходит статически, а не через потенциально гоняющийся динамический код — см. отдельно тему synchronization/thread_local в cpp_concurrency.md.Combined with thread_local, this guarantees that each thread's own instance is initialized statically rather than through potentially racy dynamic code — see the synchronization/thread_local discussion separately in cpp_concurrency.md.
constinit thread_local int t_request_depth = 0;
void on_request_enter() { ++t_request_depth; }
void on_request_exit() { --t_request_depth; }
// каждый поток получает свой t_request_depth, статически обнулённый ещё до первого доступа// each thread gets its own t_request_depth, statically zeroed before its first access
Например, нельзя просто взять адрес consteval-функции, чтобы передать его как обычный function pointer в рантайм-код — это более строгое ограничение, чем у constexpr, для которой такое обычно допустимо (constexpr-функция способна работать и как обычная рантайм-функция). Consteval-функция в принципе не существует как что-то, вызываемое в рантайме.For instance, you can't simply take the address of a consteval function to pass it as an ordinary function pointer into runtime code — this is a stricter restriction than constexpr, where that's usually fine (a constexpr function can also work as an ordinary runtime function). A consteval function fundamentally doesn't exist as something callable at runtime.
constinit int x = 5; x = 10; абсолютно валиден — constinit гарантирует только МОМЕНТ и СПОСОБ инициализации (статическая, на этапе компиляции), а не последующую неизменность значения. Тот, кто ожидает от constinit поведения const/constexpr-переменной, будет удивлён, увидев мутацию такой переменной в реальном коде.constinit int x = 5; x = 10; is perfectly valid — constinit guarantees only the MOMENT and MANNER of initialization (static, at compile time), not the value's subsequent immutability. Anyone expecting constinit to behave like a const/constexpr variable will be surprised to see such a variable mutated in real code.
constinit можно применить только к переменной, инициализатор которой в принципе является constant expression (например, вызов обычной, не constexpr/consteval, функции времени выполнения таким инициализатором не годится). Это не механизм, который "включает" compile-time вычисление того, что физически не может быть вычислено на этапе компиляции — это лишь требование к уже способному быть constant expression, диагностируемое как ошибка компиляции при нарушении.constinit can only be applied to a variable whose initializer is fundamentally capable of being a constant expression (for instance, a call to an ordinary, non-constexpr/consteval, runtime function doesn't qualify as such an initializer). It isn't a mechanism that "turns on" compile-time evaluation for something that physically can't be evaluated at compile time — it's merely a requirement on an initializer already capable of being one, diagnosed as a compile error when violated.