ядро языка — атрибутыcore language — attributes
Атрибут, сообщающий компилятору, что выражение всегда истинно в данной точке программы — без какой-либо проверки в рантайме. Компилятор вправе использовать это как аксиому при оптимизации; если предположение ложно — undefined behavior без единой диагностики.An attribute telling the compiler that an expression is always true at this point in the program — with no runtime check whatsoever. The compiler may treat it as an axiom during optimization; if the assumption is false, it's undefined behavior with zero diagnostics.
[[assume(expr)]] — атрибут, размещаемый как отдельный оператор в теле функции, сообщающий компилятору, что выражение expr ВСЕГДА истинно в этой точке программы. Компилятор ничего не проверяет — ни в debug, ни в release сборке — а вправе использовать это условие как аксиому для оптимизации: например, устранить проверку на ноль перед делением, если assume гарантирует ненулевое значение, или выбрать более узкий/быстрый путь вычисления, зная границы значения заранее.[[assume(expr)]] is an attribute placed as a standalone statement in a function body, telling the compiler that the expression expr is ALWAYS true at this point in the program. The compiler checks nothing — not in debug, not in release builds — and is free to use that condition as an axiom for optimization: for instance, eliminating a division-by-zero check when assume guarantees a nonzero value, or picking a narrower/faster computation path by knowing the value's bounds ahead of time.
void f_gcc_clang(int x) {
if (!(x > 0)) __builtin_unreachable(); // непереносимо, нечитаемо// non-portable, unreadable
int y = 100 / x;
}
void f_msvc(int x) {
__assume(x > 0); // свой интринсик, свой синтаксис// its own intrinsic, its own syntax
int y = 100 / x;
}
// три разных способа сказать одно и то же в трёх компиляторах// three different ways to say the same thing across three compilers
void f(int x) {
[[assume(x > 0)]];
int y = 100 / x; // компилятор может пропустить проверку на 0// the compiler may skip the zero check
}
// один синтаксис, одинаковый смысл на GCC/Clang/MSVC// one syntax, the same meaning across GCC/Clang/MSVC
Подсказки компилятору об "аксиомах" для оптимизации существовали и до C++23, но каждая из них была ущербной по-своему:Hints to the compiler about optimization "axioms" existed before C++23, too, but each was flawed in its own way:
__builtin_assume в Clang, __assume в MSVC, отсутствие прямого эквивалента в GCC до недавних версий — код, использующий такие подсказки, требовал #ifdef под каждый компилятор, что сводило на нет саму идею писать переносимый C++.__builtin_assume in Clang, __assume in MSVC, no direct equivalent in GCC until recent versions — code using such hints needed #ifdef per compiler, defeating the very idea of writing portable C++.
Единственный переносимый (между GCC и Clang) способ выразить "это условие всегда истинно" был обходным: превратить ложность условия в недостижимый код. Синтаксис не выражает намерение напрямую — читающий код должен мысленно вывести "ага, это assume" из отрицания и unreachable, что легко перепутать при рефакторинге.The only somewhat portable (between GCC and Clang) way to express "this condition is always true" was indirect: turning the condition's falseness into unreachable code. The syntax doesn't express the intent directly — a reader has to mentally derive "ah, this is an assume" from the negation plus unreachable, which is easy to get wrong during a refactor.
[[likely]]/[[unlikely]] из C++20 (см. cpp20/likely-unlikely.html) влияют только на то, как компилятор располагает машинный код веток условия (чтобы более вероятная ветка была "прямой" для предсказателя переходов) — они ничего не говорят о доказанной истинности условия, условие всё равно вычисляется и проверяется в рантейме. [[assume]] — принципиально другое: это заявление "условие ИСТИННО, можешь не проверять", которое компилятор использует как аксиому, а не просто как подсказку про частоту.C++20's [[likely]]/[[unlikely]] (see cpp20/likely-unlikely.html) affect only how the compiler lays out the machine code for a condition's branches (so the more probable branch is the "straight-line" one for the branch predictor) — they say nothing about the condition being proven true; the condition is still evaluated and checked at runtime. [[assume]] is fundamentally different: it's a claim that the condition IS true, and you needn't check it — the compiler treats it as an axiom, not merely a frequency hint.
[[assume(expr)]] оформляется как атрибут на пустом операторе (;) — сам по себе он не генерирует никакого кода и не вычисляет expr в рантайме. Он лишь передаёт оптимизатору информацию, которую тот может (но не обязан) использовать при генерации кода для всего, что находится "ниже по потоку" в той же функции.[[assume(expr)]] is written as an attribute on an empty statement (;) — by itself it generates no code and does not evaluate expr at runtime. It merely passes information to the optimizer, which may (but isn't obligated to) use it when generating code for everything "downstream" in that same function.
int divide(int a, int b) {
[[assume(b != 0)]]; // заявление: b никогда не равен 0 здесь// a claim: b is never 0 here
return a / b; // компилятор МОЖЕТ убрать неявную проверку на 0// the compiler MAY drop the implicit zero check
}
int clamp_positive(int x) {
[[assume(x >= 0 && x <= 100)]]; // диапазон как аксиома// a range as an axiom
return x * 2; // компилятор знает: результат в [0, 200]// the compiler knows the result is in [0, 200]
}
Важное следствие модели "аксиома без проверки": expr не обязан быть дешёвым или даже вычисляться вообще в неоптимизированной сборке — стандарт явно разрешает компилятору не вычислять само выражение (в отличие от обычного оператора-выражения), раз оно используется только как источник информации для оптимизатора, а не как код, который должен реально выполниться.An important consequence of the "axiom with no check" model: expr need not be cheap, or even evaluated at all in an unoptimized build — the standard explicitly permits the compiler to skip evaluating the expression itself (unlike an ordinary expression-statement), since it's used purely as a source of information for the optimizer, not as code meant to actually run.
void process(const std::vector<int>& v, std::size_t idx) {
[[assume(idx < v.size())]]; // v.size() может не вызваться вовсе// v.size() may not even be called
auto x = v[idx]; // компилятор может убрать неявную проверку границ// the compiler may drop the implicit bounds check
} // если idx >= v.size() в рантайме — UB, без диагностики// if idx >= v.size() at runtime — UB, no diagnostics
Функция, вызываемая только после валидации входных данных в другом месте — компилятор не может это вывести сам, но программист знает точно.A function called only after input validation elsewhere — the compiler can't deduce this itself, but the programmer knows for certain.
double normalize(double value, double scale) {
[[assume(scale > 0.0)]]; // вызывающий код уже проверил scale > 0// the caller already checked scale > 0
return value / scale;
}
Функция обработки байта, где известно, что верхние биты всегда нулевые (например, значение уже прошло через маску в другом месте) — компилятор может выбрать более компактные инструкции.A byte-processing function where the upper bits are known to always be zero (say, the value already went through a mask elsewhere) — the compiler may pick more compact instructions.
unsigned expand(unsigned x) {
[[assume(x <= 0xFF)]]; // x гарантированно помещается в один байт// x is guaranteed to fit in one byte
return x * 0x01010101u; // компилятор знает границы результата заранее// the compiler knows the result's bounds ahead of time
}
Функция, получающая указатель, который по контракту вызывающего кода всегда выровнен на границу 32 байт (например, аллоцирован через выровненный allocator) — компилятор может сгенерировать более быстрые SIMD-инструкции, не требующие проверки выравнивания.A function receiving a pointer that, by the caller's contract, is always 32-byte aligned (say, allocated via an aligned allocator) — the compiler may emit faster SIMD instructions that don't need an alignment check.
void sum_aligned(const float* data, std::size_t n) {
[[assume(reinterpret_cast<std::uintptr_t>(data) % 32 == 0)]];
// цикл ниже может быть векторизован без runtime-проверки выравнивания// the loop below can be vectorized with no runtime alignment check
for (std::size_t i = 0; i < n; ++i) { /*...*//*...*/ }
}
Код, ранее написанный с оглядкой только на GCC/Clang, переписанный на стандартный атрибут — работает одинаково на всех трёх основных компиляторах без #ifdef.Code previously written with only GCC/Clang in mind, rewritten with the standard attribute — works identically across all three major compilers with no #ifdef.
// было: #if defined(__GNUC__) || defined(__clang__) ... __builtin_unreachable() ... #endif// before: #if defined(__GNUC__) || defined(__clang__) ... __builtin_unreachable() ... #endif
enum class Mode { Read, Write };
const char* mode_str(Mode m) {
[[assume(m == Mode::Read || m == Mode::Write)]];
return m == Mode::Read ? "read" : "write";
}
В этом принципиальное отличие от assert(): assert либо реально проверяет условие и падает в рантайме в debug-сборке, либо (при NDEBUG) ничего не делает в release, полностью выключая проверку — но в обоих случаях assert НЕ даёт компилятору использовать условие как аксиому для оптимизации. [[assume]], наоборот, никогда не проверяет условие ни в одной сборке — и если оно окажется ложным в рантайме, это неопределённое поведение сразу, без malloc-подобной "хотя бы упадёт в debug" подстраховки.This is the key difference from assert(): assert either really checks the condition and aborts at runtime in a debug build, or (with NDEBUG) does nothing in release, fully disabling the check — but in neither case does assert let the compiler treat the condition as an optimization axiom. [[assume]], by contrast, never checks the condition in any build — and if it turns out false at runtime, that's undefined behavior immediately, with none of the "at least it aborts in debug" safety net.
assume для условия, которое зависит от непровалидированного пользовательского ввода или внешних данных — прямой путь к багам, срабатывающим только в оптимизированной сборке (потому что именно там компилятор реально пользуется аксиомой) и практически не воспроизводимым в debug-сборке, где assume часто не оказывает видимого эффекта. Такие баги особенно тяжело диагностировать именно из-за отсутствия любой диагностики или падения — программа просто ведёт себя неправильно, иногда молча.An assume on a condition that depends on unvalidated user input or external data is a direct path to bugs that only trigger in an optimized build (because that's where the compiler actually exploits the axiom) and are practically unreproducible in a debug build, where assume often has no visible effect. Such bugs are especially hard to diagnose precisely because there's no diagnostic or crash at all — the program just misbehaves, sometimes silently.
[[assume]] влияет только на кодогенерацию — это инструкция компилятору, а не для человека. Обычный комментарий рядом ("x гарантированно положителен, потому что...") лучше передаёт намерение читателю кода и объясняет, ПОЧЕМУ условие истинно. Не стоит путать [[assume]] с self-documenting assert — если единственная цель это задокументировать инвариант для человека, а не ускорить код, обычный комментарий или сам assert подходят лучше и безопаснее.[[assume]] only affects code generation — it's an instruction to the compiler, not to a human. An ordinary comment next to it ("x is guaranteed positive because...") conveys intent to a reader far better and explains WHY the condition holds. Don't confuse [[assume]] with a self-documenting assert — if the sole goal is documenting an invariant for a human, not speeding up the code, a plain comment or an actual assert is the better and safer fit.