обработка ошибок и compile-timeerror handling and compile-time

if consteval

Ветвление внутри constexpr-функции по признаку "вычисляемся ли мы сейчас на этапе компиляции или в рантайме" — надёжнее и синтаксически безопаснее, чем std::is_constant_evaluated() из C++20.Branching inside a constexpr function on whether it's currently being evaluated at compile time or at runtime — more reliable and syntactically safer than C++20's std::is_constant_evaluated().

часто используетсяcommonly used частый вопрос: чем отличается от if constexprcommon question: how it differs from if constexpr

Что этоWhat it is

if consteval { ... } else { ... } — специальный синтаксис внутри constexpr/consteval функции, позволяющий выбрать одну из двух веток кода в зависимости от того, вычисляется ли текущий вызов на этапе компиляции (compile-time context) или в рантайме. Ветка if consteval выполняется в compile-time контексте, ветка else — в рантайме.The if consteval { ... } else { ... } syntax inside a constexpr/consteval function lets you pick one of two code paths depending on whether the current call is being evaluated at compile time or at runtime. The if consteval branch runs in the compile-time context, the else branch runs at runtime.

до C++23 — is_constant_evaluated() внутри обычного ifbefore C++23 — is_constant_evaluated() inside a plain if
constexpr double my_sqrt(double x) {
    if (std::is_constant_evaluated()) {
        return newton_sqrt(x);  // медленно, но валидно на этапе компиляции// slow, but valid at compile time
    } else {
        return fast_bit_hack_sqrt(x);  // быстрый рантайм-путь// fast runtime path
    }
}
// обычный if — компилятор в пограничных случаях может перепутать ветку// a plain if — the compiler can pick the wrong branch in edge cases
C++23 — if consteval, специальный синтаксисC++23 — if consteval, a dedicated syntax
constexpr double my_sqrt(double x) {
    if consteval {
        return newton_sqrt(x);  // compile-time ветка// compile-time branch
    } else {
        return fast_bit_hack_sqrt(x);  // рантайм-ветка// runtime branch
    }
}
// нет условия в скобках — компилятор не может ошибиться с контекстом// no condition in parens — the compiler can't misjudge the context

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

std::is_constant_evaluated() из C++20 решала ту же задачу, но как обычная функция внутри обычного if — а обычный if живёт по правилам обычного кода, что создавало несколько источников проблем:C++20's std::is_constant_evaluated() solved the same problem, but as an ordinary function inside an ordinary if — and an ordinary if lives by the rules of ordinary code, which created several sources of trouble:

01

Агрессивный инлайнинг мог перепутать веткиAggressive inlining could confuse the branches

Поскольку is_constant_evaluated() внутри обычного if — это просто вызов функции, возвращающей bool, в пограничных случаях агрессивная инлайн-подстановка компилятора могла "перепутать" контекст — оптимизатор иногда неправильно определял, в какой ветке он на самом деле находится, что приводило к трудноуловимым багам, отличающимся между debug- и release-сборками.Since is_constant_evaluated() inside a plain if is just a call to a function returning bool, in edge cases the compiler's aggressive inlining could "confuse" the context — the optimizer sometimes misjudged which branch it was actually in, leading to hard-to-track bugs that differed between debug and release builds.

02

Не было специального синтаксиса, подсказывающего намерениеThere was no dedicated syntax to signal intent

Читая if (std::is_constant_evaluated()), ни человек, ни компилятор не сразу видят, что это принципиально другой вид ветвления, чем обычная проверка булева условия — синтаксически это неотличимо от любого другого if, что провоцирует ошибки при рефакторинге (например, случайно обернуть проверку в ! и получить противоположную, но синтаксически валидную логику).Reading if (std::is_constant_evaluated()), neither a human nor the compiler immediately sees that this is a fundamentally different kind of branching than an ordinary boolean check — syntactically it's indistinguishable from any other if, which invites refactoring mistakes (e.g. accidentally wrapping the check in ! and getting the opposite but syntactically valid logic).

03

Операции, невозможные в compile-time, нуждались в надёжном разделении путейOperations impossible at compile time needed a reliable split of paths

Такие операции, как reinterpret_cast над указателями, обращение к неинициализированной памяти ради скорости или SIMD-интринсики, либо вообще запрещены на этапе компиляции, либо валидны только в рантайме — функция должна работать в обоих контекстах, но с разной реализацией, и цена ошибки в выборе ветки — не просто медленный код, а невозможность скомпилироваться вовсе.Operations like reinterpret_cast on pointers, touching uninitialized memory for speed, or SIMD intrinsics are either outright forbidden at compile time or only valid at runtime — the function must work in both contexts but with a different implementation, and the cost of picking the wrong branch isn't just slow code, it's a failure to compile at all.

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

if consteval — это не выражение, а специальная форма if, встроенная в грамматику языка: компилятор точно знает в момент разбора кода, в какой ветке он находится, потому что сам факт "сейчас идёт constant evaluation" — это часть его собственного состояния интерпретатора, а не результат вызова функции времени выполнения. Ветка после if consteval компилируется и выполняется только при constant evaluation, ветка else — только в рантайме (либо является обязательной, либо её отсутствие означает "ничего не делать в рантайме").The if consteval construct isn't an expression but a special form of if baked into the language grammar: the compiler knows for certain, at the point of parsing, which branch it's in, because the very fact "constant evaluation is happening right now" is part of its own interpreter state, not the result of a runtime function call. The branch after if consteval compiles and runs only during constant evaluation, the else branch only at runtime (or its absence simply means "do nothing at runtime").

constexpr unsigned popcount(unsigned x) {
    if consteval {
        // compile-time: обычный цикл, компилятор развернёт и свернёт сам// compile-time: a plain loop, the compiler unrolls/folds it itself
        unsigned count = 0;
        while (x) { count += x & 1; x >>= 1; }
        return count;
    } else {
        // рантайм: аппаратный интринсик, недоступный на этапе компиляции// runtime: a hardware intrinsic, unavailable at compile time
        return __builtin_popcount(x);
    }
}

constexpr unsigned a = popcount(0b1011);  // compile-time ветка, a == 3 вычислено при компиляции// compile-time branch, a == 3 computed during compilation
unsigned b = popcount(runtime_value);      // рантайм-ветка, вызов __builtin_popcount в исполняемом коде// runtime branch, __builtin_popcount call in the emitted code

Важно, что if consteval не заменяет std::is_constant_evaluated() целиком — последняя по-прежнему существует и валидна там, где нужно проверить контекст в виде булева значения (например, передать его дальше как параметр), но новый синтаксис становится предпочтительным для собственно ветвления кода, потому что даёт компилятору однозначные гарантии для каждой ветки.Note that if consteval doesn't replace std::is_constant_evaluated() entirely — the latter still exists and is valid wherever you need the context as a plain boolean value (e.g. to pass it further as a parameter), but the new syntax becomes the preferred choice for actual code branching, because it gives the compiler unambiguous guarantees for each branch.

// синтаксические тонкости: нет скобок вокруг consteval, и отрицание не пишется через !// syntax subtleties: no parens around consteval, and negation isn't written with !
constexpr void log_if_runtime() {
    if !consteval {
        std::puts("running at runtime");  // отрицание: "if !consteval", а не "if (!is_constant_evaluated())"// negation: "if !consteval", not "if (!is_constant_evaluated())"
    }
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

constexpr sqrt с быстрым рантайм-путёмA constexpr sqrt with a fast runtime path

Классический учебный пример: рантайм-версия использует битовые трюки над представлением float (что невозможно на этапе компиляции, так как нельзя реинтерпретировать биты произвольно), compile-time версия — надёжный, но медленный алгоритм Ньютона.The classic textbook example: the runtime version uses bit tricks on the float's representation (impossible at compile time, since bits can't be arbitrarily reinterpreted), the compile-time version is a reliable but slow Newton's method.

constexpr float fast_sqrt(float x) {
    if consteval {
        float guess = x;
        for (int i = 0; i < 20; ++i) guess = 0.5f * (guess + x / guess);
        return guess;
    } else {
        return __builtin_sqrtf(x);  // аппаратная инструкция// a hardware instruction
    }
}
02

Безопасный reinterpret_cast только в рантаймеA reinterpret_cast kept safely runtime-only

Функция хеширования байтового представления объекта — reinterpret_cast к unsigned char* запрещён в constant evaluation, поэтому compile-time ветка обязана считать по-другому (например, через std::bit_cast или логическую, а не байтовую формулу).An object's byte-representation hashing function — reinterpret_cast to unsigned char* is forbidden in constant evaluation, so the compile-time branch must compute it differently (e.g. via std::bit_cast or a logical rather than byte-wise formula).

constexpr std::size_t hash_bytes(const Point& p) {
    if consteval {
        return hash_combine(p.x, p.y);  // логическая формула на полях объекта// a logical formula over the object's fields
    } else {
        auto* bytes = reinterpret_cast<const unsigned char*>(&p);
        return fnv1a(bytes, sizeof(p));  // побайтовый хэш, недоступный в constant evaluation// a byte-wise hash, unavailable in constant evaluation
    }
}
03

Диагностика/логирование только в рантаймеDiagnostics/logging kept runtime-only

Функция валидации, которая на этапе компиляции просто падает через throw в constexpr-контексте (что превращается в ошибку компиляции), а в рантайме пишет подробный лог перед тем, как бросить исключение.A validation function that at compile time simply fails via a throw in a constexpr context (which turns into a compilation error), while at runtime it writes a detailed log before throwing.

constexpr int validate_positive(int x) {
    if (x < 0) {
        if !consteval {
            log_error(std::format("negative value: {}", x));  // логирование только в рантайме// logging only at runtime
        }
        throw std::invalid_argument("negative");
    }
    return x;
}
04

Инициализация без чтения неинициализированной памяти в compile-timeInitialization avoiding uninitialized-memory reads at compile time

Буфер, который в рантайме ради скорости оставляют неинициализированным до первой записи, а в compile-time контексте (где чтение неинициализированной памяти запрещено языком) обязаны обнулить заранее.A buffer that at runtime is left uninitialized until the first write for speed, while in a compile-time context (where reading uninitialized memory is forbidden by the language) it must be zeroed up front.

constexpr std::array<int, 64> make_buffer() {
    if consteval {
        return {};  // обязательная инициализация нулями в constant evaluation// mandatory zero-initialization in constant evaluation
    } else {
        std::array<int, 64> buf;  // намеренно не инициализирован — заполняется сразу после// intentionally uninitialized — filled right after
        return buf;
    }
}

ГраблиGotchas

if consteval — не if constexpr, это два ортогональных механизмаif consteval isn't if constexpr — two orthogonal mechanisms

if constexpr (C++17) выбирает ветку по compile-time булеву условию, обычно завязанному на тип (if constexpr (std::is_integral_v<T>)) — сам выбор ветки происходит один раз, при компиляции шаблона, и не зависящая ветка даже не компилируется для данного T. if consteval (C++23) выбирает ветку по тому, где сейчас исполняется код — при вызове одной и той же функции она может пойти то в одну, то в другую ветку в зависимости от контекста вызова. Эти два измерения — "какой тип" и "какой контекст исполнения" — легко перепутать из-за похожих названий, но они решают принципиально разные задачи и иногда используются вместе в одной функции.if constexpr (C++17) picks a branch based on a compile-time boolean condition, usually tied to a type (if constexpr (std::is_integral_v<T>)) — the branch choice happens once, when the template is compiled, and the untaken branch isn't even compiled for that T. if consteval (C++23) picks a branch based on where the code is currently executing — calling the same function can go down one branch or the other depending on the calling context. These two dimensions — "which type" and "which execution context" — are easy to confuse because of the similar names, but they solve fundamentally different problems and are sometimes used together in the same function.

Синтаксис без скобок — легко написать if (consteval) по привычкеParenthesis-free syntax — easy to write if (consteval) out of habit

В отличие от if constexpr (условие), где условие обязательно в скобках, синтаксис новой конструкции — именно if consteval { ... }, без скобок вокруг consteval. Написав по аналогии if (consteval) { ... }, получаешь ошибку компиляции — это не альтернативная форма записи, а невалидный код, потому что consteval в такой позиции не является выражением.Unlike if constexpr (condition), where the condition must be in parentheses, the new construct's syntax is exactly if consteval { ... }, with no parens around consteval. Writing if (consteval) { ... } by analogy gives a compilation error — it isn't an alternative spelling, it's invalid code, because consteval in that position isn't an expression.

Работает только внутри constexpr/consteval функцийOnly works inside constexpr/consteval functions

if consteval имеет смысл только в теле функции, которая вообще может быть вычислена на этапе компиляции — то есть помеченной constexpr или consteval (либо неявно constexpr, как некоторые специальные функции-члены). Внутри обычной функции конструкция не имеет смысла и не компилируется — это не общий "определитель контекста" для произвольного кода, а инструмент именно для функций с двойной природой исполнения.if consteval only makes sense inside the body of a function that can be evaluated at compile time at all — i.e. one marked constexpr or consteval (or implicitly constexpr, like certain special member functions). Inside an ordinary function the construct is meaningless and won't compile — it isn't a general-purpose "context detector" for arbitrary code, but a tool specifically for functions with a dual execution nature.