Обновить

Контрактное программирование и C++26 все :-(

Время на прочтение1 мин
Охват и читатели107K
Всего голосов 25: ↑24 и ↓1+30
Комментарии73

Комментарии 73

А как оно должно было работать? Почитал и не особо понял. Допустим, аргумент должен быть строго больше нуля. Сейчас я делаю так:

using FooResult = std::expected<uint, FooError>;
FooResult Foo(int val)
{
    if (val <= 0)
    {
        F.Error(L"val <= 0, val: ", val, L". Check this: …");
        return std::unexpected(FooError::InvalidVal);
    }
    …
    if (!SomeApiCall(val))
    {
        F.Error(L"SomeApiCall() failed for val: ", val);
        return std::unexpected(FooError::SomeApiCallFailed);
    }
    …
    return result;
}

Пишут, мол, хорошо отделять ошибки вызова API (!SomeApiCall(val)) от логики нарушения контракта (val <= 0) и последнюю формализовать в pre, чтобы её видели все, от статанализаторов до IDE, а чем хорошо — не пишут. Зачем мне их разделять? Что практически полезного я получу?

Почитал, как там у соседей. Пишут, что в C# был System.Diagnostics.Contracts, но он тихо умер. Вместо него юзают ArgumentNullException.ThrowIfNull и атрибуты с предикатами. Кастомные атрибуты, где предикат можно было записать хотя бы строкой — вот от чего я бы не отказался в плюсах.

И как оно должно интегрироваться в написанные системы? У кого-то — локальный логгер с круговой записью, у кого-то — отстуки на сервер, у кого-то — конфигурируется для прогона тестов.

Когда я смотрел примеры (сейчас не могу найти статью) мне показалось, что контракты в предложенном виде, это свое рода комбинация static_assert и рантайм проверок.

Да, примерно так и пишут, но я практически полезных (для меня) сценариев не увидел, и поэтому не понял, надо ли огорчаться и насколько сильно.

К сожалению из за контрактов зарезали весь стандарт в том числе рефлексию

Возможно по процедуре у них была возможность голосовать только за утверждение всего стандарта целиком.

Если люди за несколько десятилетий не способны придумать или хотя бы подсмотреть (например, у Scheme) нормальную процедуру голосования, то их способность породить вменяемый язык программирования вызывает сомнения. Это же всё элементы алгоритмического мышления.

Так на этом этапе голосуют за принятие в качестве стандарта уже готового вылизанного документа. Голосование за включение туда отдельных фич происходит сильно раньше, в рамках встреч рабочей группы.

за несколько десятилетий

Уже скоро девятое пойдёт, кстати.

Ну как бы ненормально, если фичи по отдельности приняли, а итоговый документ - нет. Это очевидный изъян процедуры.

Про девятое десятилетие не понял.

Про девятое десятилетие не понял.

Это процедуры ISO, которому вот-вот 80 лет стукнет.

Ну сам по себе ISO содержит ведь и эффективные комитеты наряду с неэффективными. Например, чтобы далеко от программирования не ходить, комитет по Фортрану был в расслабленном состоянии до середины 1990-х, а в 21 веке прямо очень шустро и по делу заработал. Комитет по PL/I не сделал ровным счётом ничего за всё время существования этого языка, по Паскалю – тоже почти ничего. Так что не только в регламенте ISO дело.

Так тут комитет свою работу сделал, стандарт отклонили на голосовании стран-членов ISO.

К сожалению из за контрактов зарезали весь стандарт в том числе рефлексию

Что-то музыкой навеяло
Что-то музыкой навеяло

Ещё это похоже на завтипы, с помощью которых компилятор может часть проверок провести во время компиляции.

std::vector::operator[] нет возвращает ошибку при неправильном индексе. Именно для таких кейсов и нужны контракты. Сейчас это делает через самописные макросы внутри реализации + документации.

К тому же там в будущем хотят вроде интегрировать контракты в фундаментальные языковые конструкции. Навроде проверки указателя на nullptr (но тут могу ошибаться, для этого есть отдельный пейпер не на C++26, см. p3850r1).

Если начать проверять индексы, это убьет пооизводительность.

Если бы у бабушки были колеса, то она была бы велосипедом. Убьет 0.3%.

И никто не помнит про `.at()`...

Для этого есть отдельный метод std::vector::at

ладно, поиграем по вашим “правилам” - а для front() и back() у нас что есть?)

Я для себя ответил на этот вопрос так: нарушение контракта означает что разработчик чего то не учел, либо если это библиотека или меж модульный интерфейс, то разошлись версии. В обеих ситуациях это непредвиденное событие, а значит программа должна немедленно остановиться и показать пользователю что у разработчика проблемы и надо ему об этом сообщить (ну или что уже сообщено если есть разрешение на сбор данных). Обычный АпликэйшнЭксепшен для этого не годится. В некоторых языках эксепшены уже разделены (например в java есть RuntimeException и к нему уже относятся другие ошибки разработчика типа деления на ноль. Ловить RuntimeException без особой на то нужды - моветон, т к это приведет к маскировки серьезной проблемы, которую в 99% случаев само приложение не устранит). Поэтому я вижу контракты, как во первых удобный сахар для этого. И во вторых по хорошему контракты должны продолжать логику интерфейсов. Если в пре-кондишен записано "не ноль", а в вызове передается константой 0, то компилятор должен возапить об этом и отказаться продолжать. Если этих двух вещей нема, то это хреновая реализация контрактов. Может быть поэтому комитет и отказал (я не разбирался детально а что собственно предлагали ибо плюсами не пользуюсь )

нарушение контракта означает что разработчик чего то не учел, либо если это библиотека или меж модульный интерфейс, то разошлись версии. В обеих ситуациях это непредвиденное событие, а значит программа должна немедленно остановиться

А представьте, что Foo() из примера выше сравнивает версии, а int val это номер билда. В реальности, правда, по-другому было, не так тупо, «со спецэффектами», но для упрощения пусть будет так. Однажды в чью-то светлую голову приходит идея помечать бета-билды отрицательными номерами. Опять же, в реальности было немного иначе, но пусть будет так. Вы обновляете всех юзеров до того билда, который знает о новом контракте, проверяете, что все юзеры онлайн обновлены, и переходите на новую схему. А потом выясняется, что часть юзеров всё-таки не обновилась… с Интернетом упорные неполадки, так часто бывает, в последнее время особенно… и у них при попытке проверить версию происходит нарушение контракта. Беда-караул, «программа должна немедленно остановиться». Если же Foo() возвращает FooError::InvalidVal, вызывающий вряд ли на этом основании выйдет, логичнее пожать плечами и считать, что обновляться не на что.

Это искусственный пример, можно убрать с сервера все отрицательные билды и вернуть все заблудшие «Аполлоны» под управление ЦУПа (кроме тех, кто видя, что программа выходит при старте, сносит её и переключается на программу конкурентов), но в реальности, ещё раз говорю, всё было сложнее, и вернуть удалось не всех. Не помню, сколько юзеров потеряли безвозвратно, что-то от 10 тысяч. Это были большие деньги. Никого не уволили, но все люди взрослые, стали думать, как больше не наступать на эти грабли. И пришли к тому, что не надо так писать: «Ой, контракт нарушен, прощай, мир». Кстати, насколько помнится, не я был причиной, хотя если бы был, то с удовольствием бы забыл. Но всё-таки думаю, что не я, поскольку и до этого не понимал, зачем нужны «синие экраны», даже с дорогими коллегами жаркий спор как-то вышел.

P.S. Минус не мой.

Странно что в C# умерло, т.к. оно там играло роль не защитного программирования, а метаданных для компилятора и ide. Можно было например описать, что при входе null ответ всегда false.
И тогда припроверке вызова функции на истинность, дальге можно было использовать указатель как гарантированный не null и компилятору и ide и стат анализу было норм, а без этого, нет контракта, хз что там внутри, значит валим сборку и проверки по разименованию нала.

То что вы описываете это часть Nullable References, и оно живое.

public static bool IsNullOrEmpty([NotNullWhen(false)] String? value);

Контракты были до этого, и более продвинутые, но из-за этого же более сложные, плюс там требовало доп настроек для полноценной работы.

А как оно должно было работать?

Ну, вот как-то так и отправили на доработку. А так вообще выглядит как consteval значения и какой-нибудь panic в рантайме при нарушении контракта.

>"а чем хорошо — не пишут. Зачем мне их разделять? Что практически полезного я получу? " - Прочитал сейчас в Википедии - оказывается, я неправильно понимал "Контрактное (ПО)": как фичи, предоставляемые конкретному пользователю (страны, предприятия, физлица), согласно конкретного Контракта (заключенного) с ним (совместимое с законодательством страны, политикой организации, желанием индивидуума). Т.е. либо "так собираемое" ПО, либо runtime - фиче-управляемое. И разработка - без хардкода (и даже не столько "величин", как самой "логики"):
- "зачем (разделять логику от ошибок)?" - чтобы исходники не (сильно) менялись, при добавлении и поддержке (особенностей) др.стран;
- "что полезного (я получу)?" - более инженерный код (более блочный -> более понятный -> ...).

PS: надеюсь, что распространятся AI-сервисы на контрактной основе (в моей интерпретации), как фактор нормирования "доверия к AI" (задавать и делать "только то, что в Контракте прописано").
И читаю "не смогли договориться (по контрактам)" - как по "актуально важному вопросу".

Если почитать того же Мейера, который продвигал контракты, то там важно, что он делает сразу три вещи:

  • runtime-проверка

  • документирование (не надо смотреть в код - из заголовка функции видно, какие требования)

  • правила наследования - если виртуальная функция переопределяется, то ужесточать контракт нельзя, а ослаблять можно.

Собственно без этих трёх вещей контракты нафиг не нужны (и пользы нет, и реализуются просто так).

То что вы пишите - это защитное программирование. Только надо не исключения кидать, а панику.

Подозреваю, контрактное программирование в С++ будут использовать не для того, чтобы обнаружить нарушение контракта на вызов - а для того, чтобы "оптимизировать" сгенерированный код, типа "удалим лишние проверки, т.к. контракт не должен быть нарушен".

Рискну предположить, что это было написано мне. Если да, в этом коде не бросаются исключения. Я придерживаюсь той точки зрения, что ошибки надо обрабатывать там, где есть максимум контекста, то есть на уровне вызова сразу после вызова.

100%. Будет всегда выключенный флаг компилятора. Как с границами массивов и остальным подобным.

Если добавить явные проверки, сейчас оно так и работает.

Может все же почитать pr?

из-за чего отклонили предложения всего стандарта C++26 целиком

Чот сурово, почему просто не выкинули контракты? Там много чего ещё революционного ожидалось, рефлексия, std::execution ит.д. И от контрактов ничего из этого вроде не зависело

А там вроде уже назначили новую встречу комитета. Там наверно и вырежут лишнее.

У них встречи на год вперёд расписаны и вроде новых не назначали. Но в ноябре уже была запланирована встреча по C++29, теперь видимо на ней вместо этого будут решать, что со сложившейся ситуацией делать и выкидывать ли контракты.

Чот сурово, почему просто не выкинули контракты? Там много чего ещё революционного ожидалось, рефлексия, std::execution ит.д. И от контрактов ничего из этого вроде не зависело

Учуяли время перемен. Уходит эпоха когда язык программирования был для человека, а не для машины. Сейчас наступает эпоха когда нужен язык программирования для машины. Все эти абстракции и синтаксические сахара только мозги ей пудрят. Вобщем комитет вовремя тормознул осмотреться и не создавать лишнюю работу разработчикам компиляторов.

Все эти абстракции и синтаксические сахара только мозги ей пудрят.

Переусложнённость C++ – это один из самых первых аргументов в его критике последние лет 10.

Насколько я понял - процедура голосования предполагает только варианты - "за все" и "против всего", по за части голосования не было. Подразумевается что к этому моменту все проблемы должны быть устранены.

Казалось бы есть нормальные типы. И ассерты когда уже никак. С++ опять идет своим путем.

Нормальные типы реализуются бесплатно в рантайме, если надо. Тут проблем нет.

Да, выглядит как плохо управляемая смесь compile time (те же типы) и runtime проверок

С++ опять идет своим путем.

тем временем contract programming живет еще с восьмидесятых и C++ там не причем. https://en.wikipedia.org/wiki/Design_by_contract

Оно примерно там и осталось, поскольку добавляет опции (читай - сложностей) на ровном месте. То, что можно проверить статически проверяет тайп чекер. То, что нельзя - нужно безусловно отстреливать в рантайме. Больше сюда нечего ни добавить, ни убавить. DoC это фантазии на тему более умного тайпчекера чем у вас есть и магического рантайма в одном флаконе.

Весь CS уже был придуман в 80тых. Но не все взлетело или оказалось хорошим на практике. Контракты точно проиграли типам.

Я не понимаю, в этой ветке прочитавших определение DbC нет что ли?

Элементарная вещь, к типам отношения не имеющая.

Просто ассерты, сгруппированные в декларации функции. Вход данных, выход и инвариант.

Сразу для полноты, чтобы не утруждались Вики открывать - проверки только для отладочного режима.

Как предлагалось реализовать Комитетом, не смотрел.

Кто близок к науке носится с DbC со времен Eiffel. За эти годы было много надежд и попыток адаптировать подход в популярных языках программирования. Обычно что-то стоящее внедряется быстро. Здесь же за 40 лет нет практически ни какого прогресса. Хотя многие идеи по пути растаскали, упаковав в более удобные формы. Если вы хотите обобщенных доказательств в математическом смысле - используйте солверы теорем. Если можете довольствоваться честными случаями - пишите тесты. Примерно та же история как с функцональным программированием.

Просто ассерты, сгруппированные в декларации функции. Вход данных, выход и инвариант.

В своё время довелось поучастововать в разработке одной довольно важной и ответсвенной софтины еще когда никаких стандартов на С++ не было. И там практиковалась практика “паранаидальных ассертов” на почти всё, что можно и чуть чуть - что нельзя. В дебаге софтина еле ворочилась на топовом железе, но зато в релизе, где макрос ассертов отключался, - летала и не падала.

Это я к чему - все эти алерты работают только в купе с мощной ситемой тестирования и мешаются вне этих тестов. Так что если вы не хотите вешать тестировангие на конечных ползователей (не хотите ведь? Да?) - то зачем эти контракты?

Контракты в релизе тоже выключены

Это и есть разновидность параноидальных ассертов.

Сейчас масса тривиальных условий проверяется тайпчекерами - статически - анализаторы стали гораздо умнее. Они ловят основную часть проблем.

Проверка нетривиальных условий обычно требует дополнительных вычислений или походов за данными - наружу. Включение и выключение таких проверок само по себе влияет на поведение программы. Такие решения всегда останутся частными, а не универсальными.

DbC изначально был методологией дизайна, а не программирования (что следует из названия). То есть он предполагает много думать upfront, а кодить уже потом. Для этого в Eiffel был специфичный тулчейн, который связывал решения design time, compile time и runtime одним разработческим процессом. Штука весьма нишевая, особенно в нашем безумном мире вездесущего Agile, где принято сперва делать, а думать - потом.

Тащить в C++ это в виде отдельных огрызков нет ни какого смысла. А предлагать менять всем процессы - утопия.

То, что нельзя - нужно безусловно отстреливать в рантайме

контракты этим и заниматься (выбираете семантику enforce/quick-enforce). Но добавляют эту информацию в декларацию функции, а не реализацию. Туда, где ей и положено быть, чтобы клиент этой функции смог правильно ее использовать.

DoC это фантазии на тему более умного тайпчекера чем у вас есть и магического рантайма в одном флаконе.

откройте хоть пейпер что ли… Текущее предложение это только про рантайм и ни о каких “умных тайпчекерах” пока речь не идет (p2900r14).

ли… Текущее предложение это только про рантайм

Текущее предложение непонятно зачем. Выглядит как огрызок фичи, которую хотят зарелизить в прод, а додумывать будут потом. quick-enforce роняет компиляцию, так что не только про рантайм.

Ощущение такое, что я не помню, в котором измерении потерял нить рассуждения...

Надеемся все же на выход C++26

Интересно, начиная с 20 стандартов кто-то вообще сам на них пишет, абсолютно не читаемый код, набор кракозябр, нет никакого желания в этом разбираться.

Да вроде все понятно, на удивление они даже рефлексию, сделали так, что с первого взгляда атас, а когда уже понимаешь что к чему, понимаешь, что это ещё очень даже хорошо

Меня еще в 20 стандарте смутила реализация Ranges, у меня тогда конечно не сильно много опыта было в плюсах, но чисто визуально казалось, что в других языках гораздо удобнее реализован pipeline

А мне кажется, что начало использование модных pipe в С++ фактически открыло ящик Пандоры, так как и в без того сложный синтаксис добавилась еще одна концепция которую нужно иметь ввиду и анализировать из контекста.

Ну слушайте, все же не надо идти на поводу моды и писать переусложненные конструкции просто потому что они есть. Я бы даже сказал, куда полезнее оставаться где-то на уровне C++11 в начале жизни проекта, а когда уже вырисовывается скелет реализации - сворачивать в верхние стандарты. Учитывая развитие ИИ, такая операция будет стоить дешево.

Может вообще не надо начинать новый проект на C++ ? Сейчас столько красивых, удобных новых языков появилось, без вот этого всего мазохизма.

Специфика области - в embedded Rust ползет медленнее, и смысла имеет куда меньше чем кажется. Может я еще недостаточно опытный разраб, но каких-то особых попоболей плюсы у меня не вызывают.

“Борьба разума со здравым смыслом”, и разум, увы, побеждает… PDF про с++26 больше PDF про c++98 почти в пять раз, - язык стал в пять раз лучше? хоть по какому-то критерию?

Пора уже на раст переходить

На правах теории заговора: растовики проникли в С++ комитетты - и теперь специально искажают язык до непрограмировабельности чтобы застваить всех прейти на Раст!

Почему “теория заговора”? Пару лет назад публиковали секретный доклад по проведению политики дискредитации С++ :-)

Ну, из плюсов в 20 подвезли концепты, которые, наконец-то, добавляют вменяемую типизацию в шаблоны.

Но, по ощущениям, в последние несколько лет все просто ушли на Rust, Go и прочие штуки вроде Zig.

Но, по ощущениям, в последние несколько лет все просто ушли на …

Это преувеличенные ощущения. На сях ещё очень очень очень много чего пишется. И будет писаться ещё с десяток лет - так точно. Есть надежда, что ИИ убъёт С++ (т.к. сильно облегчит “уползание” на языки програмирования), но даже до такого сценария - ещё очень далеко.

Само собой. php, вон, уже лет 15 пророчат смерть, а он живее всех живых. Никто не будет переписывать многие миллионов строк работающего кода за несколько лет.

Но новые проекты на C++ – скорее исключение.

Никто - скорее да. А вот насчет ничто - то не факт. Процесс в общем и уже пошел:

SQLite переписали на Rust за пару часов: В июле 2026 года рой ИИ-агентов без исходного кода и интернета воссоздал знаменитую СУБД SQLite на Rust, успешно пройдя до 80% независимых тестов

Это не процесс пошёл. Это хайп полез из всех щелей: переписали sqlite, photoshop и т.д.

Вот прямо сейчас зашёл на сайт sqlite-а и там, вы не поверите, упоминается чистый Си для кода. А rust не упоминается. Грустно, да? И всё потому, что кожаные при выборе компонентов руководствуются не логикой маркетологов от ии, а какими-то своими соображениями. Обнаглели! Не дают развиваться ии! - Это был сарказм.

А если по сути, то вспоминаем про IPv4 и осознаём, что новый модный молодёжный IPv6 что-то буксует с захватом мира (а уж где там АТМ с кучей плюшек и гарантированной полосой - лучше не вспоминать). И, вы не поверите, новые проекты в сетевом программировании часто делаются с расчётом на IPv4.

Поэтому rust, конечно, хорошо. НО... Но у меня (например) большинство новых проектов делается на C++ (и работа и не-работа), оставшиеся на чистом Си (и далее в микродозах питон) - это ответ RH215, чтобы два раза не вставать. А чтобы новые проекты начинать на условном rust-е, нужно самому этот раст знать на оверхорошо. И программисты вокруг тебя тоже должны этот раст знать. И если про себя ты можешь инициативить (изучу новое, безопасное и т.д.), то с остальными это сильно тяжело.

А так-то "будем посмотреть", время покажет.

Но у меня (например) большинство новых проектов делается на C++ (и работа и не-работа), оставшиеся на чистом Си (и далее в микродозах питон) - это ответ

Дайте угодаю: у вас нет контрактов с госами США. Потому, что у кого они есть, стартуют новые проекты на Rust и занимаются переписыванием старых, даже если снаружи кажется, что в этом нет ни какого смысла.

Всего ничего, осталось какие то 20% оставшихся пройти, угу… Первые 90% работы занимают 90% времени, а оставшиеся 10% другие 90%.

С++ не моя сильная сторона. Но вот какие мысли постучались в голову...

  1. UB внутри самой проверки. Если выражение в pre/post само содержит баг (нулевой указатель, гонка), это UB в контракте. То есть инструмент, который должен повышать безопасность, сам становится источником UB — классический footgun. Вместо safety-фичи получаем ещё одну дыру.

  2. Слишком многое отдали имплементациям. Стандарт оставляет компиляторам широкую свободу в обработке нарушений. Итог — разъезжающееся поведение между тулчейнами: на Clang одно, на MSVC другое, на GCC третье. Для переносимости и предсказуемости это больно.

  3. Сложность и «магия». Синтаксис и семантика получились мутными: поведение кода зависит от флагов компиляции, взаимодействие с control flow неочевидное. Для больших кодовых баз это налог на сопровождение и риск тихих регрессий.

  4. Не покрыты важные кейсы. В первой версии нет контрактов для виртуальных функций и указателей на функции. Обещают в C++29 — но пока фича недоделана для реального продакшена.

  1. Некоторые очевидно проблемные конструкции (например, вызов неконстантных функций) в контрактах запрещены, так что это уже лучше, чем assert.

  2. Да вроде нормально. Разрешены расширения, но они и так бы были, а у четырёх стандартных режимов на мой взгляд вполне чётко поведение описано.

  3. Возможно, но это во многом и к assert и разнообразным сторонним аналогам относится, а их все используют.

  4. Виртуальные функции да, а с указателями что не так? Там для них описано ровно то поведение, какое я и хотел бы видеть: контракты функции действуют и при вызове через указатель, попытка определить новые контракты при взятии/определении указателя — синтаксическая ошибка.

Другими словами, сложность новых предложений … почитали недостаточной, из-за чего отклонили предложения всего стандарта C++26 целиком.

Забавно выстроенная фраза превращается в отличный каламбур в контексте C++.

Касательно контрактов, то, как я понимаю, они по сути ничего не гарантируют, зависят от настроек компилятора и могут быть отключены в рантайме. Ну могли бы просто отправить на доработку и убрать из спеки стандарта как они обычно это делают, но, кажется, у кого-то просто уже котелок закипел.

WG21 has been pursuing contract assertions for more than two decades

Двадцать лет я бежала за вами... Почти (с)

Двадцать лет рожать функциональность, описание которой в D или Nim занимает 1 (одну страницу), родить 63 страницы предложения канцеляритом P2900R6 и 42 страницы возражений P3846r0 и.... не принять ничего.

Мне кажется, это конец. Такой комитет нам не нужен!

Он похоронен тоннами бумаг, Астанавитесь!

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации