ядро языкаcore language

КонтрактыContracts

Нативный синтаксис языка (P2900) для предусловий и постусловий функций, а также общей формы утверждения в теле функции: pre(...), post(...) и contract_assert(...). У проверок контрактов есть настраиваемая на уровне сборки "семантика вычисления" (ignore / observe / enforce и другие), так что проверки можно включать для отладочных сборок и отключать или ослаблять для релизных.Native language syntax (P2900) for function preconditions and postconditions, plus a general assertion form inside a function body: pre(...), post(...), and contract_assert(...). Contract checks have a build-configurable "evaluation semantic" (ignore / observe / enforce and others), so checks can be compiled in for debug/test builds and stripped or downgraded for release builds.

API и точный набор семантик ещё могут измениться до ратификацииthe API and the exact set of semantics may still change before ratification частый вопрос: контракты vs assert() vs исключения/std::expectedcommon question: contracts vs assert() vs exceptions/std::expected

Что этоWhat it is

Контракты добавляют в объявление функции синтаксис для условий, которые должны выполняться до вызова (pre) и после возврата (post, с доступом к возвращаемому значению по имени). contract_assert(condition) — аналогичная проверка внутри тела функции, не привязанная к её сигнатуре. Все три формы принимают обычное булево выражение.Contracts add syntax to a function's declaration for conditions that must hold before the call (pre) and after it returns (post, with access to the return value by name). contract_assert(condition) is an analogous check inside the function body, not tied to its signature. All three forms take an ordinary boolean expression.

int at(std::vector<int>& v, size_t i)
    pre(i < v.size())            // предусловие — часть объявления, видна вызывающему коду// a precondition — part of the declaration, visible to callers
{
    return v[i];
}

Widget* create_widget(const Config& cfg)
    post(r: r != nullptr)      // постусловие — r ссылается на возвращаемое значение// a postcondition — r refers to the return value
{
    return new Widget(cfg);
}

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

Предусловия и постусловия существовали всегда — но только неформально, вне контроля компилятора и инструментов:Preconditions and postconditions always existed — but only informally, outside any compiler or tooling control:

01

До контрактов — комментарий, ручной assert() или ничегоBefore contracts — a comment, a hand-written assert(), or nothing

"// i must be < v.size()" в комментарии не проверяется никаким инструментом. Ручной assert() проверяется, но безусловно вырезается макросом NDEBUG — нет более тонкого контроля (нельзя, например, включить проверки только для части кодовой базы или логировать нарушение вместо падения)."// i must be < v.size()" in a comment is checked by no tool at all. A hand-written assert() is checked, but is unconditionally compiled out by the NDEBUG macro — there's no finer-grained control (you can't, say, enable checks for only part of the codebase, or log a violation instead of aborting).

02

Предусловие не было частью видимой сигнатуры функцииA precondition wasn't part of the function's visible signature

Условие, зарытое внутри тела функции как первая строчка с assert, не видно вызывающему коду, читающему только заголовочный файл, и недоступно статическим анализаторам как формальная часть контракта функции.A condition buried inside the function body as the first line with an assert isn't visible to calling code that only reads the header, and isn't available to static analyzers as a formal part of the function's contract.

03

Стандартизация отделяет "истинно ли условие" от "что делать при нарушении"Standardization decouples "is the condition true" from "what happens on violation"

Контракты как часть стандарта заменяют велосипед, который каждая кодовая база изобретала заново (свои макросы ASSERT/REQUIRE/ENSURE), одним механизмом, где вычисление условия отделено от политики реакции на его нарушение (семантики), задаваемой на уровне сборки.Contracts as part of the standard replace the wheel every codebase reinvented on its own (its own ASSERT/REQUIRE/ENSURE macros) with one mechanism, where evaluating the condition is decoupled from the policy for reacting to its violation (the semantic), set at the build level.

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

Каждая проверка контракта имеет "семантику вычисления", выбираемую флагом сборки, а не в исходном коде: ignore — условие вообще не вычисляется (как assert под NDEBUG); observe — условие вычисляется, при нарушении вызывается обработчик (по умолчанию — сообщение и продолжение выполнения, без остановки программы); enforce — условие вычисляется, при нарушении программа завершается. Точный набор имён семантик — деталь предложения, которая может ещё уточняться до ратификации; общая идея — "проверять или нет" и "что делать при провале" настраиваются независимо друг от друга и независимо от исходного текста.Every contract check has an "evaluation semantic," chosen by a build flag rather than in the source code: ignore — the condition isn't evaluated at all (like assert under NDEBUG); observe — the condition is evaluated, and on violation a handler is invoked (by default, a message and continued execution, no program termination); enforce — the condition is evaluated, and on violation the program terminates. The exact set of semantic names is a proposal detail that may still be refined before ratification; the general idea — "check or not" and "what to do on failure" are configured independently of each other and independently of the source text.

// contract_assert — проверка в теле функции, не связанная с сигнатурой// contract_assert — a check inside the function body, unrelated to the signature
void quicksort_partition_step(std::vector<int>& v, size_t lo, size_t hi, size_t pivot_index) {
    reorder_around_pivot(v, lo, hi, pivot_index);
    contract_assert(is_partitioned(v, lo, hi, pivot_index));  // инвариант, который иначе был бы тихим багом// an invariant that would otherwise be a silent bug
}

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

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

at() с pre() вместо ручного assert в начале функцииat() with pre() instead of a hand-written assert at the top

Условие корректности индекса становится частью объявления функции, а не первой строкой её тела — видно из заголовка, без необходимости заглядывать в реализацию.The index-validity condition becomes part of the function's declaration rather than the first line of its body — visible from the header, with no need to look at the implementation.

template<typename T>
T& at(std::vector<T>& v, size_t i)
    pre(i < v.size())   // заменяет "assert(i < v.size());" внутри тела функции// replaces "assert(i < v.size());" inside the function body
{
    return v[i];
}
02

post() на фабричной функции — возвращаемый объект никогда не nullpost() on a factory function — the returned object is never null

post(r: r != nullptr) обещает вызывающей стороне, что фабрика либо вернёт валидный указатель, либо контракт будет нарушен (и обработан согласно текущей семантике) — не нужно проверять результат на null вручную, если предполагается, что этого не должно случиться.post(r: r != nullptr) promises the caller that the factory either returns a valid pointer or the contract is violated (and handled per the current semantic) — no need to manually check the result for null if it's assumed this shouldn't happen.

Widget* create_widget(const Config& cfg)
    post(r: r != nullptr)
{
    return new Widget(cfg);   // new бросает исключение при нехватке памяти, а не возвращает nullptr// new throws on out-of-memory rather than returning nullptr
}
03

contract_assert() посреди алгоритма для проверки инвариантаcontract_assert() mid-algorithm to check an invariant

В сортировке разбиение массива вокруг опорного элемента — инвариант, нарушение которого без явной проверки осталось бы тихим и трудноуловимым багом, проявляющимся только на некорректно отсортированном результате в самом конце.In a sort, the array staying partitioned around the pivot is an invariant whose violation, without an explicit check, would be a silent and hard-to-catch bug, only surfacing as an incorrectly sorted result at the very end.

void quicksort(std::vector<int>& v, size_t lo, size_t hi) {
    if (lo >= hi) return;
    size_t p = partition(v, lo, hi);
    contract_assert(is_partitioned_around(v, lo, hi, p));  // иначе баг проявится только в итоговой сортировке// otherwise the bug would only show up in the final sort
    quicksort(v, lo, p);
    quicksort(v, p + 1, hi);
}
04

Граница библиотеки: pre и post вместе, разные семантики в тестах и в релизеA library boundary: pre and post together, different semantics for tests and release

Тесты собираются с семантикой enforce, чтобы ловить нарушения контракта рано и явно; production-сборка — с observe или ignore для нулевых накладных расходов в горячем пути.Tests are built with the enforce semantic to catch contract violations early and explicitly; the production build uses observe or ignore for zero overhead on the hot path.

double compute_interest_rate(double principal, double years)
    pre(principal > 0.0)
    pre(years > 0.0)
    post(r: r >= 0.0)
{
    return apply_rate_model(principal, years);
}
// сборка тестов: -fcontract-evaluation-semantic=enforce  (имя флага условно)// test build: -fcontract-evaluation-semantic=enforce  (flag name illustrative)
// релизная сборка: -fcontract-evaluation-semantic=ignore — нулевые накладные расходы// release build: -fcontract-evaluation-semantic=ignore — zero overhead

ГраблиGotchas

При семантике ignore нарушенное предусловие — по-прежнему UBUnder the ignore semantic, a violated precondition is still UB

Контракты — механизм проверки, включаемый по желанию, а не автоматическая гарантия безопасности. Если сборка использует семантику ignore, условие вообще не вычисляется — нарушение предусловия остаётся неопределённым поведением ровно как если бы контракта не было вовсе.Contracts are an opt-in checking mechanism, not an automatic safety guarantee. If the build uses the ignore semantic, the condition isn't evaluated at all — a violated precondition remains undefined behavior exactly as if there had been no contract at all.

Условия контракта могут быть дорогими — выбирайте семантику осознанноContract conditions can be expensive — choose the semantic deliberately

Условие в pre/post/contract_assert — произвольное выражение с побочными эффектами и любой сложностью. O(n)-проверка в предусловии, вызываемая на каждой итерации горячего цикла под семантикой enforce, способна незаметно посадить производительность — выбор семантики для каждого типа сборки должен быть осознанным, а не по умолчанию "включено везде".A condition in pre/post/contract_assert is an arbitrary expression, with side effects and any level of complexity. An O(n) check in a precondition, evaluated on every iteration of a hot loop under the enforce semantic, can silently tank performance — the semantic for each build type has to be a deliberate choice, not "on everywhere" by default.

Контракты — не замена исключениям/std::expectedContracts are not a replacement for exceptions/std::expected

Контракты выражают ошибки программиста — нарушенный инвариант означает, что вызывающий код неправ. Исключения и std::expected выражают ожидаемые, восстановимые сбои времени выполнения — отсутствующий файл не является багом. Смешивание этих двух назначений (например, использование pre() для проверки пользовательского ввода, которое может законно провалиться) — распространённая концептуальная ошибка.Contracts express programmer errors — a violated invariant means the calling code is wrong. Exceptions and std::expected express expected, recoverable runtime failures — a missing file is not a bug. Conflating the two purposes (e.g. using pre() to validate user input that can legitimately fail) is a common conceptual mistake.