Что такое контракт в программировании

Проектирование по контракту

Два года назад мне посчастливилось побывать на лекции замечательного человека, одного из разработчиков языка Eiffel, Бертрана Мейера. Он читал в нашем университете (СПб ГУ ИТМО) лекцию о довольно интересной концепции проектирования ПО. Называется она «проектирование по контракту». Суть этой концепции я попытаюсь описать ниже.

Вот, например, когда вы с клиентом договариваетесь о совместной работе, то вы заключаете контракт. Т.е. вы описываете обязанности обоих сторон и возможные последствия в случае неожиданных ситуаций. Данный подход можно применить и к разработке ПО, где в качестве сторон выступают программные модули.
Проектирование по контракту является довольно простой, но, в то же время, мощной методикой, основанной на документировании прав и обязанностей программных модулей для обеспечения корректности программы. Я считаю, что корректная программа – это программа, которая выполняет не больше и не меньше того, на что она претендует.
Центральными фигурами этой концепции являются утверждения (assertions) – это булевы выражения, описывающие состояние программы. Можно выделить три основных типа утверждений: предусловия, постусловия и инварианты класса.

Предусловия.
Предусловия – это требования подпрограммы, т.е. то, что обязано быть истинным для выполнения подпрограммы. Если данные предусловия нарушены, то подпрограмма не должна вызываться ни в коем случае. Вся ответственность за передачу «правильных» данных лежит на вызывающей программе.
Этот подход противоречит многим концепция, которым учат в огромном количестве учебников. Там постоянная проверка вынесена во главу угла. Проектирование по контракту утверждает обратное – лишние проверки могут только навредить. Вообще, принцип проектирования по контракту «смотрит» на проектирования с позиции «Сложность – главный враг качества» (но об этом в следующий раз 😉 ).

Постусловия
Постусловия выражают состояния «окружающего мира» на момент выполнения подпрограммы. Т.е. это условия, которые гарантируются самой подпрограммой. Кроме того, наличие постусловия в подпрограмме гарантирует ее завершение (т.е. не будет бесконечного цикла, например).

Инварианты класса
Инварианты – это глобальные свойства класса. Они определяют более глубокие семантические свойства и ограничения целостности, характеризующие класс. Класс гарантирует, что данное условие всегда истинно с точки зрения вызывающей программы.

Попробую сформулировать основную идею, которой пользуюсь я:
«Если клиент, вызывающий подпрограмму, выполняет все предусловия, то вызываемая подпрограмма обязуется, что после ее выполнения все постусловия и инварианты будут истинными».

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

Чтобы не быть голословным приведу пример (здесь и далее весь код написан на C#).
Давайте рассмотрим такую ситуацию: пользователь вводит код зап. части по каталогу и хочет получить информацию об этой детали. Известно, что код состоит из 9 символов. Вот классический пример реализации данной функции:

Во многих классических учебниках по созданию «качественного ПО» этот пример назвали бы отличным. Но вот с точки зрения принципа проектирования по контракту этот пример является ошибочным.
Начнем с того, что проверку на валидность значения атрибута id должна осуществлять вызывающая программа. Ведь именно она (вызывающая программа) может воспользоваться несколькими вариантами: завершить работу, выдать предупреждение и начать считывать новое число. А может существует возможность вводить только последние 4 цифры, а первые пять программа сформирует исходя из VIN-номера автомобиля. В любом случае, какой бы вариант не использовался, он ни как не связан с функцией GetComponentInfo().
Тогда исходный пример перепишем в следующем виде

public ComponentInfo GetComponentInfo( string id)
<
return componentProvider.GetComponent(id);
>

А вот дальше начинается самое интересное :). Если уж мы заявили, что данная функция возвращает объект типа ComponentInfo, то мы должны обеспечить это. Ведь метод GetComponent объекта componentProvider может вернуть значение null. И тогда уже вызывающей программе придется делать проверку на null-значение, иначе можем «нарваться» на «object reference» исключение. Т.е. пример стоит переписать так:

По крайней мере, так говорится во многих статьях и примерах. НО. Давайте рассуждать логически. Если уж мы используем принцип проектирования по контракту, то, опираясь на мое «золотое правило», мы можем быть уверены, что метод GetComponent() объекта componentProvider вернет нам истинное значение (т.к. его параметр по определению истинный). Поэтому, я не вижу смысла загромождать программу лишним кодом. Но с другой стороны, объект типа ComponentProvider может быть спроектирован сторонним разработчиком, который не придерживался принципа проектирования по контракту. Вот тут и встает дилемма. Вот мой совет для данной ситуации – если вы вызываете подпрограмму, которая была написана вами, то не пишите лишнего кода. Доверяйте себе. Но если вы вызываете подпрограмму, написанную сторонним разработчиком, и вы не уверены в ней, то произведите проверку. Самый наглядный пример – функция извлечения квадратного корня Math.Sqrt(). Понятно, что нельзя извлечь квадратный корень из отрицательного числа, но если в данную функцию передать отрицательное число, то никакого исключения сгенерировано не будет, а вернется значение типа NaN.

Все приведенные примеры основываются на некоторых ваших (команды разработчиков) соглашениях. Но существуют и специальные расширения для различных языков программирования. Например, препроцессор iContract для Java или расширение eXtensible C#.

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

Источник

Контракты для кода содержат классы для маркировки кода, статический анализатор для анализа во время компиляции и анализатор времени выполнения. Классы для контрактов для кода можно найти в пространстве имен System.Diagnostics.Contracts.

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

Улучшенное тестирование. Контракты для кода обеспечивают проверку статических контрактов, проверку во время выполнения и создание документации.

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

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

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

средства и подробные инструкции по использованию контрактов кода см. в разделе контракты кода на сайте Visual Studio marketplace.

Preconditions

Предусловия можно выразить с помощью метода Contract.Requires. Предусловия задают состояние при вызове метода. Обычно они используются для указания допустимых значений параметров. Все члены, упомянутые в предусловиях, должны быть не менее доступны, чем сам метод; в противном случае предусловие может быть не понято всеми объектами, вызывающими метод. Условие не должно иметь побочных эффектов. Поведение невыполненных предусловий во время выполнения определяется анализатором времени выполнения.

Например, следующее предусловие указывает, что параметр x не должен иметь значение null.

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

Устаревшие операторы Requires

эти операторы появляются перед всеми остальными операторами в методе;

после всего набора таких операторов следует явный вызов метода Contract, например вызов метода Requires, Ensures, EnsuresOnThrow или EndContractBlock.

Постусловия

Постусловия — это контракты для состояния метода при его завершении. Постусловие проверяется непосредственно перед выходом из метода. Поведение невыполненных постусловий во время выполнения определяется анализатором времени выполнения.

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

Стандартные постусловия

Стандартные постусловия можно выразить с помощью метода Ensures. Постусловия выражают условие, которое должно быть true при нормальном завершении метода.

Исключительные постусловия

Особые постусловия

Следующие методы могут использоваться только в постусловиях.

Значение должно существовать в состоянии предусловия метода. Чтобы сослаться на поле объекта, предусловия должны гарантировать, что объект всегда имеет значение, отличное от NULL.

Нельзя ссылаться на возвращаемое значение метода в старом выражении:

Нельзя ссылаться на параметры out в старом выражении.

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

Старое выражение не может ссылаться на параметр анонимного делегата в вызове метода ForAll или Exists, если он не используется как индексатор или аргумент вызова метода:

Старое выражение не может возникать в тексте анонимного делегата, если значение старого выражения зависит от какого-либо параметра этого анонимного делегата, кроме случая, когда этот анонимный делегат является аргументом метода ForAll или Exists:

В настоящее время средства анализа контрактов для кода не проверяют правильность инициализации параметров out и игнорируют их упоминание в постусловии. Таким образом, если в предыдущем примере в строке после контракта использовалось значение x вместо назначения ей целого числа, компилятор не будет выдавать правильную ошибку. Однако в сборке, в которой не определен символ препроцессора CONTRACTS_FULL (например, в сборке выпуска), компилятор выдаст ошибку.

Инварианты

Инварианты объектов — это условия, которые должны выполняться для каждого экземпляра класса всегда, когда этот объект является видимым для клиента. Они выражают условия, при которых объект считается правильным.

Методы инвариантов идентифицируются по пометке атрибутом ContractInvariantMethodAttribute. Методы инвариантов не должны содержать никакой код, кроме последовательности вызовов метода Invariant, каждый из которых определяет отдельный инвариант, как показано в следующем примере.

Инварианты условно определяются по символу препроцессора CONTRACTS_FULL. При проверке во время выполнения инварианты проверяются в конце каждого открытого метода. Если инвариант упоминает открытый метод в том же классе, проверка инварианта, которая обычно происходит в конце этого открытого метода, будет отключена. Вместо этого проверка будет выполняться только в конце самого внешнего вызова метода для этого класса. Это также происходит, если класс повторно вводится в результате вызова метода в другом классе. Инварианты не проверяются на метод завершения объекта и IDisposable.Dispose реализацию.

Правила использования

Упорядочение контрактов

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

Чистота

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

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

Методы, помеченные атрибутом PureAttribute.

Типы, помеченные атрибутом PureAttribute (этот атрибут относится ко всем методам типа).

Методы доступа get свойства.

Операторы (статические методы, имена которых начинаются с op, имеющие один или два параметра и тип возвращаемого значения, отличный от void).

Любой метод, полное имя которого начинается с System.Diagnostics.Contracts.Contract, System.String, System.IO.Path или System.Type.

Любой вызванный делегат, при условии, что сам тип этого делегата помечен атрибутом PureAttribute. Типы делегата System.Predicate и System.Comparison считаются чистыми.

Видимость

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

Пример

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

Источник

Контрактное программирование в PHP

Введение

Сама идея контрактного программирования возникла в 90-х годах у Бертрана Мейера при разработке объектно-ориентированного языка программирования Eiffel. Суть идеи Бертрана была в том, что нужно было иметь инструмент для описания формальной верификации и формальной спецификации кода. Такой инструмент давал бы конкретные ответы: «метод обязуется сделать свою работу, если вы выполните условия, необходимые для его вызова». И контракты как нельзя лучше подходили для данной роли, потому что позволяли описать что будет получено от системы (спецификация) в случае соблюдения предусловий (верификация). С тех пор появилось множество реализаций данной методики программирования как на уровне конкретного языка, так и в виде отдельных библиотек, позволяющих задавать контракты и проводить их верификацию с помощью внешнего кода. К сожалению, в PHP нет поддержки контрактного программирования на уровне самого языка, поэтому реализация может быть выполнена только с помощью сторонних библиотек.

Контракты в коде

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

Предусловия

Самым простым вариантом контракта являются предусловия — требования, которые должны быть выполнены перед конкретным действием. В рамках ООП все действия описываются методами в классах, поэтому предусловия применяются к методами, а их проверка происходит в момент вызова метода, но до выполнения самого тела метода. Очевидное использование — проверка валидности переданных параметров в метод, их структуры и корректности. То есть с помощью предусловий мы описываем в контракте все то, с чем мы точно работать не будем. Это же здорово!

Чтобы не быть голословным, давайте рассмотрим пример:

Мы видим, что метод пополнения баланса в неявном виде требует числового значения величины суммы пополнения, которая также должна быть строго больше нуля, в противном случае будет выброшено исключение. Это типичный вариант предусловия в коде. Однако он имеет несколько минусов: мы вынуждены искать глазами эти проверки и, находясь в другом классе, не можем быстро оценить наличие/отсутствие таких проверок. Также, без наличия явного контракта, нам придется помнить о том, что в коде класса есть необходимые проверки входящих аргументов и нам не надо волноваться за них. Еще один фактор: эти проверки выполняются всегда, как в режиме разработки, так и боевом режиме работы приложения, что незначительно влияет в отрицательную сторону на скорость работы приложения.

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

Постусловия

Следующая категория контрактов — постусловия. Как можно догадаться из названия, данный тип проверки выполняется после того, как было выполнено тело метода, но до момента возврата управления в вызывающий код. Для нашего метода deposit из примера мы можем сформировать следующее постусловие: баланс счета после вызова метода должен равняться предыдущему значению баланса плюс величина пополнения. Осталось дело за малым — описать все это в виде утверждения в коде. Но вот здесь нас поджидает первое разочарование: как же сформировать это требование в коде, ведь мы сперва изменим баланс в теле самого метода, а потом попытаемся проверить утверждение, где нужно старое значение баланса. Здесь может помочь клонирование объекта перед выполнением кода и проверка пост-условий:

Еще одно разочарование поджидает нас при описании постусловий для методов, возвращающих значение:

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

Инварианты

Новые возможности

Давайте посмотрим на то, как можно использовать контракты с помощью этого фреймворка:

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

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

Благодаря использованию АОП стало возможным реализовать даже инварианты — они элегантно описываются в виде аннотаций Invariant в док-блоке класса и ведут себя аналогично постусловиям, но для всех методов.

Во время экспериментов с кодом я обнаружил удивительное сходство контрактов с интерфейсам в PHP. Если стандартный интерфейс определят требования к стандарту взаимодействия с классом, то контракты позволяют описывать требования к состоянию инстанса класса. Применяя описание контракта в интерфейсе, удается описывать требования как к взаимодействию с объектом, так и к состоянию объекта, которое будет потом реализовано в классе:

Дальше начинается самое интересное: при создании класса и определении нужного метода любая современная IDE переносит все аннотации из описания метода в интерфейсе в сам класс. А это позволяет движку PhpDeal их находить и обеспечивать автоматическую проверку контрактов в каждом конкретном классе, реализующем данный интерфейс. Для желающих пощупать все собственными руками — можно скачать проект с гитхаба, установить все зависимости с помощью композера, настроить локальный веб-сервер на эту папку и потом просто открыть в браузере код из папки demo

Заключение

Контрактное программирование в PHP — абсолютно новая парадигма, которая может использоваться для защитного программирования, для улучшения качества кода и обеспечения читаемости контрактов, определяемых в виде требований и спецификаций. Большой плюс данной реализации в том, что код классов остается читаемым, сами аннотации читаются как документация, а также то, что в боевом режиме проверка может быть полностью отключена и не требует абсолютно никакого времени на дополнительные ненужные проверки в коде. Интересный факт: сам фреймоврк содержит лишь пару аннотаций и один класс аспекта, который связывает эти аннотации с конкретной логикой.

Источник

Пробуем контрактное программирование С++20 уже сейчас

Что такое контракт в программировании. Смотреть фото Что такое контракт в программировании. Смотреть картинку Что такое контракт в программировании. Картинка про Что такое контракт в программировании. Фото Что такое контракт в программировании

В С++20 появилось контрактное программирование. На текущий момент ни один компилятор ещё не реализовал поддержку этой возможности.

Но есть способ уже сейчас попробовать использовать контракты из C++20, так как это описано в стандарте.

Есть форк clang, поддерживающий контракты. На его примере я рассказываю как пользоваться контрактами, чтобы как только фича появилась в вашем любимом компиляторе, вы сразу же могли начать её использовать.

Про контрактное программирование уже написано много, но в двух словах расскажу что это такое и для чего нужно.

Логика Хоара

В основе парадигмы контрактов лежит логика Хоара (1, 2).

Логика Хоара – это способ формального доказательства корректности алгоритма.
Она оперирует такими понятиями, как предусловие, постусловие и инвариант.
С практической точки зрения, использование логики Хоара это, во-первых, способ формального доказательства корректности программы в тех случаях, когда ошибки могут привести к катастрофе или гибели людей. Во-вторых, способ повысить надёжность программы, наряду со статическим анализом и тестированием.

Контрактное программирование

Основная идея контрактов в том, что по аналогии с контрактами в бизнесе, для каждой функции или метода описываются договорённости. Эти договорённости должны соблюдать как вызывающая сторона, так и вызываемая.
Неотъемлемой частью контрактов является как минимум два режима сборки – отладочный и продуктовый. В зависимости от режима сборки контракты должны себя вести по разному. Наиболее распространённой практикой является проверка контрактов в отладочной сборке и их игнорирование в продуктовой.

Иногда в продуктовой сборке контракты тоже проверяются и их невыполнение может, например, вести к генерации исключения.

Основное отличие использования контрактов от «классического» подхода в том, что вызывающая сторона должна соблюдать предусловия вызываемой стороны, которые описываются в контракте, а вызываемая должна соблюдать свои постусловия и инварианты.
Соответственно, вызываемая сторона не обязана проверять корректность передаваемых её параметров. Эта обязанность возлагается контрактом на вызывающую сторону.

Несоблюдение контрактов должно быть обнаружено на этапе тестирования и дополняет все виды тестов: модульные интеграционные и т. д.

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

Итак, какую пользу дают контракты:

Контрактное программирование в C++

Контрактное программирование реализовано во многих языках. Наиболее яркие примеры, это Eiffel, где парадигма была впервые реализована, и D, в D контракты являются частью языка.

В C++, до стандарта C++20, контракты можно было использовать в виде отдельных библиотек.

Такой подход имеет ряд недостатков:

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

Использование контрактов в таком виде, действительно делает код уродливым и нечитаемым. Это одна из причин, почему использование контрактов в C++ мало практикуется.

Забегая вперёд, покажу как в C++20 будет выглядеть использование контрактов.
А затем, разберём всё это подробнее:

Пробуем

К сожалению, на текущий момент ни один из широко используемых компиляторов ещё не реализовал поддержку контрактов.
Но есть выход.

ARCOS research group из Universidad Carlos III de Madrid реализовали экспериментальную поддержку контрактов в форке clang++.

Чтобы не «писать код на бумажке», а иметь возможность сразу же попробовать новые возможности в деле, мы можем собрать этот форк и с его помощью пробовать приводимые ниже примеры.

Инструкция по сборке описана в readme репозитория на Гитхабе
https://github.com/arcosuc3m/clang-contracts

У меня не возникло проблем при сборке, но компиляция исходников занимает очень много времени.

Для компиляции примеров вам нужно будет явно указать путь к бинарнику clang++.
Например, у меня это выглядит примерно так

Я подготовил примеры, чтобы вам было удобно исследовать контракты на примерах реального кода. Предлагаю, прежде чем приступить к чтению следующего раздела, склонировать и скомпилировать примеры.

Здесь /path/to/clang++ путь к бинарнику clang++ вашей сборки экспериментального компилятора.

Кроме самого компилятора, ARCOS research group подготовили свою версию Compiler Explorer для своего форка.

Контрактное программирование в C++20

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

Как уже было сказано выше, контракты строятся из предусловий, постусловий и инвариантов (утверждений).

В C++20 для этого используются атрибуты со следующим синтаксисом

Где contract-attribute может принимать одно из следующих значений:
expects, ensures или assert.

expects используется для предусловий, ensures для постусловий и assert для утверждений.

conditional-expression – это булево выражение, проверяемый в контракте предикат.
modifier и identifier могут быть опущены.

Зачем нужен modifier я напишу чуть ниже.

identifier используется только с ensures и служит для представления возвращаемого значения.

Предусловия имеют доступ к аргументам.

Постусловия имеют доступ к возвращаемому функцией значению. Для этого используется синтаксис

Где return_variable любое валидное выражение для переменной.

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

Считается, что предусловия и постусловия являются частью интерфейса функции, в то время как утверждения являются частью её реализации.

Предикаты предусловий всегда вычисляются непосредственно перед выполнением функции. Постусловия выполняются сразу же после передачи функцией управления вызывающему коду.

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

Предусловия и постусловия всегда описываются вне тела функции и не могут иметь доступ к локальным переменным.

Если предусловия и постусловия описывают контракт для публичного метода класса, они не могут иметь доступ к приватным и защищённым полям класса. Если метод класса защищённый, то к защищённым и публичным данным класса доступ есть, а к приватным нет.
Последнее ограничение совершенно логично, если учесть, что контракт является частью интерфейса метода.

Утверждения (инварианты) всегда описываются в теле функции или метода. По дизайну они являются частью реализации. И, соответственно, могут иметь доступ ко всем доступным данным. В том числе, к локальным переменным функции и приватным и защищённым полям класса.

Определим два предусловия, одно постусловие и один инвариант:

Предусловие публичного метода не может ссылаться на защищённое или приватное поле:

Не допускается модификация переменных внутри выражений, описываемых атрибутами контракта. Если это нарушено, будет UB.

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

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

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

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

Это означает, что в первую очередь всегда проверяются предусловия, как проиллюстрировано в следующем примере:

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

В этом случае можно опустить идентификатор возвращаемого значения.

Если постусловие ссылается на аргумент функции, то этот аргумент рассматривается в точке выхода из функции, а не в точке входа, как в случае с предусловиями.

Нет никакого способа ссылаться на оригинальное (в точке входа в функцию) значение в постусловии.

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

Например, для constexpr функции нельзя ссылаться на локальные переменные, если только они не известны во время компиляции.

Контракты для указателей на функцию

Нельзя определить контракты для указателя на функцию, но указателю на функцию можно присвоить адрес функции, для которой определён контракт.

Вызов pfoo(100) приведёт к нарушению контракта.

Контракты при наследовании

Классическая реализация концепции контрактов предполагает, что предусловия могут быть ослаблены в подклассах, постусловия и инварианты могут быть усилены в подклассах.

В реализации C++20 это не так.

Во-первых, инварианты в C++20 являются частью реализации, а не интерфейса. По этой причине, их можно как усилить, так и ослабить. Если в реализации виртуальной функции assert отсутствует, то он не будет унаследован.

Во-вторых, требуется, чтобы при наследовании функции были ODR идентичны.
А, поскольку предусловия и постусловия являются частью интерфейса, то в наследнике они должны в точности совпадать.

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

К сожалению, пример выше не работает в экспериментальном компиляторе как ожидается.

Если у foo из Derived2 опустить контракт, то он не будет унаследован из базового класса. Кроме того, компилятор позволяет определить для подкласса контракт несовпадающий с контрактом базового.

Ещё одна ошибка экспериментального компилятора:

синтаксически правильной должна быть запись

Однако в таком виде я получил ошибку компиляции

и пришлось заменить на

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

Модификаторы контрактов

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

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

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

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

В нашем случае, это опция компилятора

Программа может быть скомпилирована с тремя разными уровнями проверки:

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

В нашем случае, для этого используется опция компилятора

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

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

Перехват нарушения контракта

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

При компиляции можно установить обработчик violation handler, вызываемый при нарушении контракта.

Способ реализации установки обработчика отводится на усмотрение создателей компилятора.

В нашем случае это

Сигнатура обработчика должна иметь вид

std::contract_violation эквивалентна следующему определению:

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

Если обработчик violation handler задан, то, в случае нарушения контракта, по умолчанию, сразу после его выполнения будет вызван std::abort() (Без указания обработчика вызывается std::terminate() ).

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

Способ реализации этих средств остаётся на усмотрение разработчиков компилятора.
В нашем случае, это опция компилятора

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

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

Это связано с возможностью компилятора выполнять низкоуровневые оптимизации в рассчёте на выполнение контрактов.

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

Определим свой обработчик и с его помощью перехватим нарушение контракта

И рассмотрим пример нарушения контракта:

Теперь можно привести примеры, демонстрирующие поведение программы при нарушении контракта при разных уровнях сборки и режимах контрактов.

И сборка с уровнем audit даст:

Замечания

violation_handler может бросать исключения. В этом случае можно настроить программу так, чтобы нарушение контракта вело к выбросу исключения.

Заключение

Контракты относятся к неинтрузивным проверкам времени выполнения. Они играют очень важную роль в обеспечении качества выпускаемого программного обеспечения.

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

Контракты C++20 позволят сделать наши программы ещё более надёжными, быстрыми и понятными. С нетерпением жду их реализацию в компиляторах.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *