В C++26 появились контракты. Создатель D говорит, что от них мало толку
C++26 добавляет pre(...), post(...) и contract_assert(...), но стандарт позволяет каждому компилятору решать, будет ли нарушенный контракт проигнорирован, записан в лог или завершит программу.
Контракты позволяют функции заявить, что должны гарантировать вызывающие и что она обещает взамен. Нарушение можно проигнорировать, наблюдать, принудительно проверить или проверить с быстрым завершением. Выбор делается на уровне сборки, переносимого переключателя для отдельного утверждения нет. Руководство предупреждает, что контракты не заменяют валидацию входных данных.
Уолтер Брайт, который говорит, что добавлял контракты в Zortech C++ в 1990-х и позже в D, написал, что они «оказались не очень полезными». Встраиваемый разработчик хочет обратного: проверки включены на рабочей станции и выключены на слабых производственных процессорах.
Три ключевых слова и что происходит при нарушении
Руководство C++ Stories проходит по функции. pre(...) ставится после списка параметров и задаёт то, что вызывающий обязан гарантировать на входе. post(result: ...) задаёт, что функция обещает при обычном возврате, и одно постусловие покрывает все пути возврата. contract_assert(...) проверяет свойство в произвольной точке тела функции. Контракты не входят в тип функции, поэтому разрешение перегрузки их не учитывает.

Что происходит при нарушении, зависит от семантики вычислений. При ignore предикат не вычисляется. При observe нарушение вызывает обработчик, и выполнение продолжается, если он вернулся. Enforce вызывает обработчик и завершает программу, если тот вернулся. Quick-enforce завершает программу без вызова обработчика. Реализация сама выбирает, какие семантики поддерживать, а в C++26 нет переносимого способа выбрать одну для отдельного утверждения. GCC 16 компилирует их с флагами -std=c++26 -fcontracts -fcontract-evaluation-semantic=enforce.
Руководство перечисляет ловушки. Предикаты не должны менять состояние, но ничто не мешает им вызывать функцию, которая его меняет. Постусловия не проверяются, если функция выходит через исключение. В режимах observe и ignore недопустимая операция всё равно может привести к неопределённому поведению. А дорогие предикаты, например проверка, что вся коллекция отсортирована, способны замедлить программу.
Брайт считает, что контракты мало полезны, а практики не согласны
«Раньше я был большим поклонником контрактов и даже реализовал их в компиляторе Zortech C++ ещё в 90-х… Потом сделал их и в D. Но оказалось, что они не очень полезны.» — Уолтер Брайт, который, по его словам, встроил контракты и в Zortech C++, и в D
Ответ задал очевидный вопрос: почему бесполезны, если сделать их правильно, они бы переносили многие случаи на этап компиляции и помогали оптимизатору? В других местах треда люди, которые используют эту идею каждый день, описали, что они получают. Комментатор, работающий во встраиваемых системах, ответил скептику рантайм-проверок:
«Я работаю во встраиваемых системах. В продакшене наши процессоры недостаточно мощны, чтобы рисковать всеми этими проверками во время выполнения, но я хочу включать их на рабочей станции, в надежде что, если будет проблема, мы её поймаем.» — разработчик встраиваемых систем, ответивший скептику runtime-утверждений
Комментатор с 19 тысячами кармы добавил, что предусловия и постусловия в Clojure дают «небольшое, но заметное улучшение по сравнению с обычными assert», а возможность их выключить: «фича, а не баг». В треде не приведены причины Брайта, но эти комментаторы показывают, что получают от идеи те, кто ею пользуется.
«Мутно» против «размытость старше контрактов»
Комментатор с 22 тысячами кармы увидел проблему в самом предупреждении руководства: контракты не валидация входных данных и не валидация параметров, это «проверки, которые выдают ошибку, но это не ошибки, которые мы порождаем». Он также хотел, чтобы контракты превращали ошибки времени выполнения в ошибки на этапе компиляции. А то, что предлагает стандарт, по его словам, это assert, который обещают «на самом деле проверять (наверное)».
Два ответа разобрали этот аргумент с разных сторон. Комментатор с 21 тысячей кармы сказал, что размытость появилась раньше контрактов, потому что то же различие относится к утверждениям:
«Ошибка утверждения (а значит, и ошибка контракта) указывает на баг. Она никогда не должна происходить, поэтому подавлять проверку всегда допустимо.» — комментатор с 21 тысячей кармы
Комментатор, использующий предусловия и постусловия Clojure, ответил на желание проверок на этапе компиляции: принудить такие условия к проверке при компиляции значит ввести вторую систему типов с уточняющими типами, а предусловие вида collatz(x)==1 потребовало бы от компилятора доказать гипотезу Коллатца. Третья формулировка, комментатора с 33 тысячами кармы, провела границу по возможности: «Если это может случиться, но не должно, это валидация входных данных», а «если не может случиться и не должно, это контракт».
Случай, когда контракты менее безопасны, чем assert
Самую сильную атаку на саму фичу дал комментатор, разобравший модель «призрачного кода» из спецификации. По спецификации нельзя полагаться на то, что проверка контракта действительно выполняется, даже если вы попросили компилятор её запустить: проблемы ABI и ODR (правила одного определения) могут удалить проверку. Из этого он сделал вывод:
«Люди ошибочно безусловно превращают №1 в №2, не понимая, что это на самом деле неправильное использование контрактов. Это может и будет вести к неопределённому поведению.» — комментатор, разобравший модель призрачного кода в спецификации
Здесь №1: assert(p != nullptr); assert(p->some_value());, а №2: то же самое, но оба вызова заменены на contract_assert: второе выражение тогда может разыменовать нулевой указатель, если первая проверка не выполнилась. Он предсказал, что через десять лет совет будет таким: избегать контрактов в коде, критичном для безопасности. Если поставить это рядом с разработчиком из встраиваемых систем, получится одна и та же фича, прочитанная двумя способами: для одного это сетка безопасности на этапе разработки, для другого опасность, если нельзя быть уверенным, что с ней делает конкретная сборка.
Где их разместить и можно ли оптимизатору им верить
Читатель пожаловался, что контракты в заголовках тянут за собой «пол-мира» инклудов. Ответивший сказал, что первое объявление должно нести условия, а проверки, которым нужна внутренняя область видимости, могут вызывать функции в других местах. Другой предложил модули, третий сказал, что ответы придут через пару лет от тех, кто встроил контракты в реальную кодовую базу.
По оптимизации мнения разошлись на факты. Разработчик, написавший собственный компилятор C, сказал, что компиляторы будут оптимизировать, предполагая, что контракты выполняются: «Конечно, в этом и идея». Разработчик ПО с PhD по физике ответил, что это «не первоочередная цель», а семантики «assume» обсуждали, но в стандартизированные они не вошли. Если второй прав, бесплатная информация о производительности, о которой спрашивал комментатор, не входит в то, что стандартизирует C++26.
Получайте такие разборы каждый день
Главные истории Hacker News — на email, в Telegram или RSS.
Фальшивый эпизод South Park в Sonarr оказался инфостилером на 887 МБ
У торрента было 2 392 сида, и выглядел он как обычный релиз. Sonarr не мог его импортировать, поэтому повторял попытку каждые 90 секунд, а qBittorrent сообщал, что загрузка завершена, с 0 байт.