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




Александр Ганюхин

Orion Innovation

Ноябрь 2021

Как появился доклад?

Как появился доклад?

  • Более 13 лет пишу на C++

Как появился доклад?

  • Более 13 лет пишу на C++
  • За первые 7 лет не написал ни одного assert

Как появился доклад?

  • Более 13 лет пишу на C++
  • За первые 7 лет не написал ни одного assert
  • 6 лет назад познал assert-дзен

Как появился доклад?

  • 6 лет назад познал assert-дзен

Как появился доклад?

  • 6 лет назад познал assert-дзен
  • 5 лет назад познакомился с контрактным программированием

Как появился доклад?

  • 6 лет назад познал assert-дзен
  • 5 лет назад познакомился с контрактным программированием
  • Уже 4 года жду контракты в C++...

Как появился доклад?

  • 6 лет назад познал assert-дзен
  • 5 лет назад познакомился с контрактным программированием
  • Уже 4 года жду контракты в C++20...

Как появился доклад?

  • 6 лет назад познал assert-дзен
  • 5 лет назад познакомился с контрактным программированием
  • Уже 4 года жду контракты в C++2023...

Зачем ждать

если можно рассказать

Напишем функцию

Напишем функцию

Но

Но

std::optional

Но...

throw

Но...

Улучшенная функция

Улучшенная функция

Усугубим проблему

Усугубим проблему

Усугубим проблему

Усугубим проблему

Добавим условие

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

Вернёмся к Но

Вернёмся к Но

Иными словами

Заключен контракт

Иными словами

Заключен контракт

  1. Пользователь вызывает функцию правильно

Иными словами

Заключен контракт

  1. Пользователь вызывает функцию правильно
  2. Функция выполняет необходимую задачу

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

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

  • Термин введён Бертраном Мейером как часть дизайна языка Эйфель в 1986

Бертран Мейер

2 столпа
контрактного программирования

2 столпа

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

2 столпа

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

Контракт

  • Контракт — обмен обещаниями между вызывающей и вызываемой стороной:

Контракт

  • Контракт — обмен обещаниями между вызывающей и вызываемой стороной:
    • Вызывающая сторона обещает соблюсти все условия вызова функции предусловия

Контракт

  • Контракт — обмен обещаниями между вызывающей и вызываемой стороной:
    • Вызывающая сторона обещает соблюсти все условия вызова функции предусловия
    • Вызываемая сторона обещает выполнить действия, приводящие к определенному состоянию постусловию (если все предусловия были соблюдены)

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


							/**
							 * @brief    Вычисляет площадь фигуры
							 */
							float square(Figure * figure);
						

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


							/**
							 * @brief    Вычисляет площадь фигуры
							 *
							 * @pre      figure — валидный указатель
							 * @post     Площадь фигуры вычислена правильно
							 */
							float square(Figure * figure);
						

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


							/**
							 * @brief    Вычисляет площадь фигуры
							 *
							 * @pre      figure — валидный указатель
							 * @post     Площадь фигуры вычислена правильно
							 *
							 * Заключен контракт:
							 */
							float square(Figure * figure);
						

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


							/**
							 * @brief    Вычисляет площадь фигуры
							 *
							 * @pre      figure — валидный указатель
							 * @post     Площадь фигуры вычислена правильно
							 *
							 * Заключен контракт:
							 *    Пользователь обещает передавать валидный указатель,
							 */
							float square(Figure * figure);
						

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


							/**
							 * @brief    Вычисляет площадь фигуры
							 *
							 * @pre      figure — валидный указатель
							 * @post     Площадь фигуры вычислена правильно
							 *
							 * Заключен контракт:
							 *    Пользователь обещает передавать валидный указатель,
							 *    Функция обещает вычислить площадь корректно
							 */
							float square(Figure * figure);
						

Контракт

  • Постусловие будет достигнуто только если предусловие — истинно

Контракт

  • Постусловие будет достигнуто только если предусловие — истинно
  • Условия контракта не должны нарушаться в корректной программе

Вы уже знакомы

С библиотечными функциями

std::lower_bound(fst, lst, value)
The range [fst, lst) must be partitioned

Вы уже знакомы

С библиотечными функциями

std::lower_bound(fst, lst, value)
The range [fst, lst) must be partitioned
std::includes(fst1, lst1, fst2, lst2)
Both ranges must be sorted

Вы уже знакомы

С библиотечными функциями

std::lower_bound(fst, lst, value)
The range [fst, lst) must be partitioned
std::includes(fst1, lst1, fst2, lst2)
Both ranges must be sorted
std::unique(fst, lst)
The behavior is undefined if "==" is not an equivalence relation

Вы уже знакомы

С библиотечными функциями

Вы уже знакомы

Что если
контракт нарушен?

Помните?

  • Постусловие может быть достигнуто только если истинно предусловие

контракт нарушен

  • Постусловие может быть достигнуто только если истинно предусловие
  • Если предусловие нарушено, то функция может делать всё что угодно

контракт нарушен

  • Постусловие может быть достигнуто только если истинно предусловие
  • Если предусловие нарушено функция может делать всё что угодно
  • Undefined behavior, но не всегда тот самый undefined behavior из стандарта

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

контракт нарушен

  • Должен ли я всегда проверять соблюдение контракта?
  • Должен ли я всегда проверять соблюдение контракта? — Нет
  • Должен ли я всегда проверять соблюдение контракта? — Нет
  • Могу ли я проверить соблюдение
    контракта?
  • Должен ли я всегда проверять соблюдение контракта? — Нет
  • Могу ли я проверить соблюдение контракта? — Да

assert

Вернёмся к примеру

Контракт
и assert

Лирическое отступление об assert

assert в llvm-project

assert в llvm-project

Включая тесты

assert в llvm-project

Включая тесты

  • Количество assert: 121'000
  • Количество файлов: 42'000

    2,9 assert на файл

  • Количество строк кода: 10'700'000

    Каждая 88 строка содержит assert

  • Максимальное количество assert в файле: 5'632

assert в llvm-project

Исключая тесты и примеры

assert в llvm-project

Исключая тесты и примеры

  • Количество assert: 40'000
  • Количество файлов: 14'000

    2,9 assert на файл


  • Количество строк кода: 6'000'000

    Каждая 150 строка содержит assert

  • Максимальное количество assert в файле: 902

assert в folly

assert в folly

  • Количество assert: 840
  • Количество файлов: 1'700

    2 assert на файл


  • Количество строк кода: 500'000

    Каждая 600 строка содержит assert

  • Максимальное количество assert в файле: 68

Контракт
и assert

Контракт
и assert

Контракт
и assert

Контракт
и assert

Контракт
и assert

Контракт C++

C++ contracts

C++ contracts

C++ contracts

Помните?

  • Если предусловие нарушено, то функция может делать всё что угодно

Помните?

  • Если предусловие нарушено, то функция может делать всё что угодно
  • (Например, вызывать std::abort, если включена проверка контрактов)

C++ contracts

  • Показаны согласно P2182R1, P2388R3
  • Не контракты для C++20
  • Возможно контракты для C++23

C++ contracts P2388R3

C++ contracts P2388R3

Attribute-based

Новый синтаксис

P2388R3

Attribute-based

P2461R0

Closure-based

В презентации используются

Attribute-based

Предусловие

Предусловие

  • Предикат(условие), который всегда должен быть истинным перед выполнением некоторого раздела кода

Предусловие

  • Предикат(условие), который всегда должен быть истинным перед выполнением некоторого раздела кода
  • Если условие ложно, то эффект раздела с кодом становится неопределённым

Предусловие

  • Предикат(условие), который всегда должен быть истинным перед выполнением некоторого раздела кода
  • Если условие ложно, то эффект раздела с кодом становится неопределённым
  • Чаще всего предусловие просто описано в документации (например, doxygen)

Предусловие

  • Предикат(условие), который всегда должен быть истинным перед выполнением некоторого раздела кода
  • Если условие ложно, то эффект раздела с кодом становится неопределённым
  • Чаще всего предусловие просто описано в документации (например, doxygen)
  • Или используют механизмы языка (assert, или же специальный синтаксис)

Предусловие

  • Делегирование ответственности за проверку условий вызывающей стороне (они становятся предусловиями)

Предусловие в Eiffel

Предусловие в doxygen

Предусловие в C++

Предусловие в C++

Постусловие

Постусловие

  • Предикат(условие), который должен быть истинным после выполнения некоторого раздела кода

Постусловие

  • Предикат(условие), который должен быть истинным после выполнения некоторого раздела кода
  • Эффект функции

Постусловие

  • Предикат(условие), который должен быть истинным после выполнения некоторого раздела кода
  • Эффект функции
  • Чаще всего постусловие просто описано в документации (например, doxygen)

Постусловие

  • Предикат(условие), который должен быть истинным после выполнения некоторого раздела кода
  • Эффект функции
  • Чаще всего постусловие просто описано в документации (например, doxygen)
  • Или используют механизмы языка (assert, или же специальный синтаксис)

Постусловие в Eiffel

Постусловие в doxygen

Постусловие в C++

Постусловие в
C++ contracts

Особенности

Особенности разработки

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

Дополнительная нагрузка

  • Помощь в статическом анализе
  • Помощь компилятору

Помощь в статическом анализе

Помощь в статическом анализе

Помощь компилятору

Помощь компилятору

Помощь компилятору

Обратите внимание

  • Не все предусловия и постусловия можно выразить кодом

Только документация

Только документация

Позаключаем контракты?

Избежать UB

Избежать UB

Контракт

Проверка

Избежать UB

Контракт

Проверка

Избежать UB

Контракт

Узкий (narrow)

Проверка

 

Избежать UB

Контракт

Узкий (narrow)

Проверка

Широкий (wide)

Некорректный результат

Некорректный результат

Контракт

Проверка

Некорректный результат

Узкий контракт

Широкий контракт

Ослабление предусловия

Чем слабее(шире) предусловие

  • тем больше состояний должна поддерживать функция (например, проверка входных данных)
  • тем меньше обязательств накладывается на вызывающую сторону

Ослабленное предусловие

Однако такая функция

  • Всегда должна производить проверки
  • Даже если входные данные всегда корректны

Проверять или доверять?

  • Не задача контрактного программирования
  • Всё зависит от необходимого и возможного дизайна

Инвариант

Инвариант

  • Набор условий(утверждений), которые обязаны быть истинными на протяжении всей жизни объекта — для классов, на протяжении всего цикла (а так же до и после) — для циклов

Инвариант класса

  • Конструктор инициализирует объект класса в валидное состояние

Инвариант класса

  • Конструктор инициализирует объект класса в валидное состояние
  • Методы класса "уверены", что объект находится в валидном состоянии на момент вызова

Инвариант класса

  • Конструктор инициализирует объект класса в валидное состояние
  • Методы класса "уверены", что объект находится в валидном состоянии на момент вызова
  • Методы класса оставляют объект в валидном состоянии

Инвариант класса

  • Конструктор инициализирует объект класса в валидное состояние
  • Методы класса "уверены", что объект находится в валидном состоянии на момент вызова
  • Методы класса оставляют объект в валидном состоянии
  • Деструктор проверяет, что объект находился в валидном состоянии

Инвариант класса

  • Проверка инварианта — дополнительное обязательное предусловие и постусловие метода

Инварианты в C++

Инварианты в C++

Инварианты в C++

Подведём итог

Мы узнали, что

Мы узнали, что

  • Контрактное программирование рассматривает функцию как контракт

Мы узнали, что

  • Контрактное программирование рассматривает функцию как контракт
  • Где вызывающий обязуется соблюсти условия вызова — предусловия

Мы узнали, что

  • Контрактное программирование рассматривает функцию как контракт
  • Где вызывающий обязуется соблюсти условия вызова — предусловия
  • А функция обязуется выполнить набор действий, приводящий к определенному итогу — постусловию

Мы узнали, что

  • Контрактное программирование рассматривает функцию как контракт
  • Где вызывающий обязуется соблюсти условия вызова — предусловия
  • А функция обязуется выполнить набор действий, приводящий к определенному итогу — постусловию
  • Поведение функции не определено в случае, если предусловие нарушено

Мы узнали, что

  • Условия контракта не должны нарушаться в корректной программе

Мы узнали, что

  • Условия контракта не должны нарушаться в корректной программе
  • Удаление контракта (или отключение проверок) не должно влиять на поведение программы

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

  • Разгружает функцию, упрощая её реализацию

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

  • Разгружает функцию, упрощая её реализацию
  • Разгружает функцию, получая большую производительность

Заключайте

Контракты

Ссылки

p2388r3

p2461r0