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

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

Ядерный реактор и зонд дальнего космоса. Часть I

автор Мэтью Уилсон 1 января 2006 года


Краткие сведения Контрактное программирование-это то. Что существует уже давно. Но в последнее время получает гораздо больше эфирной игры и строгой проверки. Несмотря на общее согласие относительно привлекательности программных контрактов для программистов (и пользователей!). Остается неясным. Что делать. Когда контракты нарушаются. Это первая часть серии. Которая немного философски рассматривает этот важный вопрос. Рассматривает компромиссы между информацией и безопасностью при реагировании на нарушения контрактов. Рассматривает практические меры по закрытию ошибочных процессов и вводит новую технику для реализации неустранимых исключений в C++.

Ядерный реактор и зонд дальнего космоса. Часть 1

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

Часть 1

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

Он также рассматривает вопрос наблюдения—кто определяет и обнаруживает (в)правильность—и подчеркивает Принцип Удаляемости. Наконец, в нем рассматривается часто неправильно понимаемая взаимосвязь между исключительными условиями. Неверными данными и нарушениями контрактов. Подчеркивается тот факт. Что исключения являются инструментом для использования при реализации программ правильной обработки. А также механизмом отчетности о нарушениях контрактов.

Часть 2

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

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

Часть 3

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

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

Часть 4

Заключительная часть продолжает практическую направленность своей предшественницы. Во—первых, он вводит неустранимый класс исключений для C++. Который делает именно то. Что написано на жестянке: однажды брошенный. Он может быть пойман. Чтобы облегчить изящное завершение процесса. Но его эффект не может быть погашен-завершение неизбежно. Затем серия статей завершается рассмотрением Контрактное программирование получило свое первое или. По крайней мере. Самое тщательное и широко признанное толкование Бертраном Мейером в его новаторской книге 1], где оно было известно как 2]. (Примечание: термин Последний излюбленный термин Контрактное программирование, как было предложено Уолтером Брайтом в 2004 году и использовано в недавнем предложении Торстена Оттосена и Лоуренса Кроула органу по стандартам C++ [ 3].)

Использование метафоры контракта в программной инженерии-растущий. Но не вполне понятный феномен. Контракт на программное обеспечение сам по себе. Как и в жизни. Является лишь соглашением (явным или иным) между вовлеченными сторонами. Он 4Метафора программного контракта охватывает не только функциональное поведение—типы. Интерфейсы. Параметры и возвращаемые значения и т. Д., — но и операционное поведение—сложность, скорость. Использование ресурсов и т. Д.

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

Контрактное программирование — это поиск ошибок в вашем программном обеспечении. Звучит потрясающе? Ну, скажем по-другому. Речь идет о поиске недостатков дизайна. Теперь это звучит еще более потрясающе! Как работает компилятор—хороший комплект, конечно. Но все же очень хороший. глупая вещь по сравнению с человеком (отдел маркетинга. несмотря на это)—предполагается. Что он способен понять ваш замысел и сделать это лучше, чем вы? В конце концов. Вполне вероятно. Что ни один другой инженер-программист, даже гуру. Не поймет ваш дизайн даже так хорошо, как вы. Не говоря уже о том. Чтобы лучше. Поэтому, конечно. Компилятор не может. Вы должны проложить для него тропу.

Точно так же. Как человеческий язык содержит избыточность и механизмы проверки ошибок. Мы должны гарантировать. Что наш код делает то же самое. Вы сообщаете компилятору. Каков ваш дизайн. И он гарантирует. Что каждый раз. Когда он берет крошку. Он проверяет дизайн.

По сути, контрактное программирование заключается в определении дизайна с точки зрения поведения ваших компонентов (функций и классов) и утверждении истин о дизайне вашего кода в виде тестов времени выполнения. Размещенных в нем. Эти утверждения истины будут проверяться по мере того. Как нить выполнения проходит через части ваших компонентов. И будут (Примечание: не все части договоров поддаются кодификации на современных языках. И есть некоторые споры о том. Могут ли они когда-либо быть [4Это не умаляет ценности контрактного программирования. Но определяет пределы его активной реализации в коде. В этой статье я сосредоточусь на практических преимуществах кодификации контрактных программных конструкций.)

Поведение определяется в терминах предварительных условий функции/метода. Постусловий функции/метода и инвариантов класса. (Есть некоторые тонкие вариации на эту тему. Такие как инварианты процесса. Но все они разделяют одни и те же основные понятия с этими тремя элементами.) Предварительные условия указывают. Какие условия должны быть истинными для выполнения функции/метода в соответствии с его дизайном. Удовлетворение предварительных условий является обязанностью вызывающего. Постусловия говорят. Какие условия будут существовать после выполнения функции/метода в соответствии с его дизайном. Удовлетворение постусловий является обязанностью вызываемого. Инварианты класса указывают. Какие условия справедливы для того. Чтобы класс находился в состоянии. В котором он может функционировать в соответствии со своим замыслом; инвариант-это свойство согласованности. Которому должен удовлетворять каждый экземпляр класса всякий раз. Когда он наблюдаем извне4]. Инварианты классов должны быть проверены после построения. До разрушения. А также до и после вызова каждой публичной функции-члена.

Давайте начнем с рассмотрения простой функцииstrcpy(), которая реализуется в соответствии с:

char *strcpy(char *dest. Char const *src) { char *const r = dest; for(;; ++dest. ++src) { if(�\0� == (*dest = *src)) { 

Каковы его пред-/постусловия? Пусть N— число символов. На которые указывает srcтот, который не содержит завершающего нуль символа �\0� (0).

Некоторые предварительные условия таковы:

  • src указывает на последовательность из N + 1 символов (типаchar), значение каждого из которых доступно выражению *(src + n). Где n-целое число в диапазоне [0, N + 1)
  • dest указывает на блок памяти. Доступный для записи длиной N + 1 (или более) символов.

Некоторые постусловия таковы:

  • Для каждого n в диапазоне [0, N + 1) справедливо выражение *(src + n) == *(dest + n)
  • Возвращаемое значение — это значение. Переданное в destпараметре

Предварительные условия

Мы можем увидеть. Что предварительное условие проверено следующим образом:

char *strcpy(char *dest. Char const *src) { char * r; /* Проверки предварительных условий */ assert(IsValidReadableString(src)); /* Is src valid? */ assert(IsValidWriteableMemory(dest, 1 + strlen(src)); / * Is dest valid? */ for(r = dest;; ++dest. ++src) { if(�\0� == (*dest = *src)) { 

где:

  • assert() является ли стандартный макрос C. Который вызываетabort(), если его выражение вычисляет false (0)
  • IsValidReadableString() является условным системным вызовом. Который проверяет. Ссылается ли указатель на строку с нулевым завершением. Содержимое которой охватывает доступную для чтения память
  • strlen() является стандартной функцией C. Которая возвращает количество символов в строке с нулевым окончанием
  • IsValidWriteableMemory() это условный системный вызов. Который проверяет. Что указатель ссылается на определенный размер записываемой памяти.

Обратите внимание. Что на практике тесты предварительных условий фактически не выполняются перед функцией. Скорее они находятся внутри реализации функции. Но перед любой из операций. Выполняемых функцией.

Когда тест на исполнение контракта. Такой как assert()s в примере. Терпит неудачу, говорят , что он срабатывает, и говорят. Что код нарушил свой контракти находится в нарушенном состоянии или недействительном состоянии. По определению. Запуск нарушения контракта в компоненте-это сообщение от автора(авторов) кода. Которое точно и абсолютно заявляет, что компонент нарушил свою конструкцию. И никаких будущих ожиданий относительно его поведения не может быть сделано. И никаких гарантий не дано. Как указывает Кристофер Диггинс [5], �недостатки конструкции носят транзитивный характер. В программной инженерии нет известного метода. Способного предсказать. Что обнаруженный дефект дизайна в определенной области не повредил дизайн остальной части программного обеспечения. Включая другие утверждения, потенциально заставляя их ложно принимать неправильные контрактыЯ рассмотрю этот вопрос с некоторой строгостью в части 2 в отношении нарушений предварительных условий и их потенциальной возможности восстановления.

Любая методология . Которая использует программные контракты и обеспечивает соблюдение их условий с помощью тестов во время выполнения. Должна учитывать три важных элемента обеспечения соблюдения: Обнаружение, Отчетность и реагирование. При использовании assert() этих трех эффективной работы в одном месте обнаружения состоит из условного теста на заданное выражение; отчетность включает в себя вызов fprintf() или эквивалент для отображения информации. Обычно включающий имя файла + номер строки. Неудачные выражения. И, возможно. Некоторые дополнительные уточняющие сведения (см. Главу 1 несовершенном языке C++ [6]); ответ это завершение через вызов exit()или abort(). Важно понимать. Что в примерах. Приведенных в этой части. Использование assert()является деталью реализации. Всего лишь одним из средств осуществления принудительного исполнения и весьма периферийным по отношению к самому контракту. Контракты могут исполняться и другими способами. Например с помощью исключений. Которые мы рассмотрим в части 4.

Постусловия

Как я уже говорил в главе 1 книги Imperfect C++ [ 6], реализация постусловий в C++ в целом является нетривиальной задачей и опирается на некоторые прыжки с обруча. В этом случае это будет выглядеть следующим образом:

#if defined(ACMELIB_TEST_POSTCONDITIONS) static char *strcpy_impl(char *dest. Char const *src); char *strcpy(char *dest. Char const *src) { char *const d = dest; char const *const s = src; char *r; /* Проверка предварительных условий */ assert(IsValidReadableString(src)); assert(IsValidWriteableMemory(dest, 1 + strlen(src))); /* Вызов r = strcpy_impl(dest. Src); /* Проверки постусловий. */ assert(0 == strcmp(d. S)); /* Все содержимое одинаковое? */ assert(r == d); /* Он вернул правильное назначение? */ static char *strcpy_impl(char *dest. Char const *src) #else /* ? ACMELIB_TEST_POSTCONDITIONS */ char *strcpy(char *dest. Char const *src) #endif /* ACMELIB_TEST_POSTCONDITIONS */ { 

Причина разделения на внутреннюю и внешнюю функции заключается в том. Что тесты должны быть вне (внутреннего) контекста функции. Чтобы автор тестов мог быть уверен. Что он видит истинное постусловие. Это особенно важно в C++. Где деструкторы локальных объектов scope могут влиять на постусловия после их якобы

На практике. Как правило. Тестирование посткондиционирования остается в слишком жесткой корзине. За исключением исключительных случаев. Когда преимущества перевешивают трудности. (Одной из таких хлопот в этом случае было бы обеспечение тогоstrlen(), чтобы не звонить strcpy() в противном случае у нас может возникнуть небольшая проблема со стеком.) Обратите внимание. Что в принципе нет почти никакой разницы между проверкой постусловия функции в функции-оболочке. Как показано выше. Или в фактическом клиентском коде функции. Просто первый случай выполняется только (один раз) автором библиотеки. Который должен это делать. А второй-пользователем библиотеки. Который может быть не осведомлен о полном поведенческом спектре и/или устарел в отношении изменений в действительном поведении с тех пор. Как они написали свои тесты.

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

Это просто оставляет инвариантное тестирование. Точно так же. Как мы видели с постусловиями. Мы не можем легко произвести проверку инвариантов до и после вызова метода. Но мы можем легко организовать их выполнение в начале и в конце вызова метода. Давайте рассмотрим простой тип, Dollarкласс:

класс Доллар { Публично: явный доллар(int dollars. Int cents) Публично: Dollar &add(Dollar const &rhs); Dollar &add(int dollars. Int cents); public: int getDollars() const; // returns # of dollars int getCents() const; // returns # of cents int getAsCents() const; // returns total amount in cents Частное: int m_dollars; 

Учитывая этот очень простой класс. Что мы можем сказать и сделать о его инварианте? Что ж, поскольку в принципе можно иметь или быть обязанным любую сумму денег. Мы можем сказать. Что допустимый диапазон долларов-это все. Что может храниться в ан int. (Если у вас больше $2B. Вы можете выбрать a long long.) Однако доллары. Будь то австралийские. Канадские или любые другие страны. У которых они есть. Имеют только 100 центов за доллар. Таким образом. Мы можем сказать. Что инвариант для нашего Dollarкласса состоит в том. Что центы не должны быть больше 99. Следовательно. Мы могли бы написать наш инвариант в privateфункции-членеis_valid():

класс Доллар { . . . private: bool is_valid() const { if( m_cents 

Обратите внимание. Что это предполагает, что поле центов всегда положительно, а отрицательные суммы представлены m_dollarsтолько в знаке, например, $-24.99 будет представлено как m_dollars= -24, m_cents= 99. Если бы мы решили представлять негативность в общей сумме в обоих членах. Наш инвариант должен был бы отражать и это. Если бы мы это сделали. Мы также смогли бы больше сказать в нашем инварианте о связи между отрицательными значениями переменных-членов:

bool Dollar::is_valid() const { { return false; } if((m_cents 

Давайте посмотрим. Как мы зацепим инвариант:

Dollar & Dollar::add(int dollars. Int cents) { assert(is_valid()); // Проверка инварианта при вводе метода // . . .. код для добавления двух сумм . .. . assert(is_valid()); // Проверка инварианта при выходе метода 

Обратите внимание. Что мы показываем стратегию утверждения при вызовах инвариантов. Показанных здесь. Вместо того. Чтобы сама инвариантная функция запускала утверждения. В случае сложных классов также часто можно увидеть. Что некоторые отчеты происходят внутри инвариантной функции. В то время как утверждение применяется к возвращаемому значению. Дальнейшие рассуждения на эту тему см. в главе 1 книги Imperfect C++ [ 7]. is_valid()Метод и его тесты определяют и применяют критерии для Dollarрепрезентативного контракта: это инвариант представления. Просто: если is_valid() возвращает false, значит. В долларе либо допущена ошибка проектирования. Либо он был поврежден (либо из-за необнаруженного нарушения предварительного условия. Либо из-за того. Что какая-то другая часть обработки вторглась в его память). Альтернативный взгляд на определение инвариантов — это публичный инвариант. Примером для Dollarэтого может служить:

Dollarэкземпляра d либо выражение d.getDollars() + d.getCents() == d.getAsCents() && g.getCents() имеет значение true. Если d.getDollars()возвращает неотрицательное значение. Либо выражение d.getDollars() - d.getCents() == d.getAsCents() && g.getCents() имеет значение true.

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

Существует общее мнение. Что контрактное программирование-это хорошо. И на самом деле большинство из нас уже много лет занимается той или иной формой контрактного программирования. Поскольку мы используем утверждения для обеспечения соблюдения предположений о дизайне нашего кода. Контрактное программирование-это методология для однозначного определения нарушения дизайна программного обеспечения с использованием конструктов. Встроенных в него его автором(ами). Которые являются единственными квалифицированными специалистами для такого определения. И, в значительной степени. Она обеспечивает в коде то. Что раньше обычно выражалось только через документацию. Это дает значительные преимущества с точки зрения качества кода. Я буду обсуждать практические примеры этого в частях 2 и 4. А пока давайте рассмотрим теоретическую перспективу. Рассматривая постусловия.

Полное постусловие — это эквивалентное описание алгоритма. Используемого в функции. Но написанное ортогональным образом. Например, если у вас есть функция сортировки. Проверка ее постусловия проверит. Что входной массив действительно отсортирован в соответствии с намерением функции [8]. Теперь предположим. Что существует вероятность 90%. Что алгоритм сортировки реализован правильно. И вероятность 90%. Что постусловие было написано правильно. Если спецификации ортогональны. Вероятность сбоя обоих на одних и тех же данных составляет 10%. Умноженное на 10%, или 1%. Таким образом. Написав вдвое больше кода. Мы можем добиться в 10 раз большей надежности. В этом и заключается настоящая магия контрактного программирования. Это та же идея. Что и в конструкции самолета. Имея независимые коробки. Контролирующие критическую задачу. Контроллеры имеют разную электронику. Разные процессоры. Разные алгоритмы. И программное обеспечение написано разными командами. Они голосуют за ответ. И система автоматически отключается. Если они не согласны. Именно так достигается очень высокая надежность. Хотя сам по себе каждый контроллер далеко не является высоконадежным.

Обсуждение до сих пор в значительной степени охватывает основные концепции контрактного программирования и вполне достаточно. Чтобы служить основой для дискуссий. Которые я хочу представить в этой статье. Однако это всего лишь царапина на поверхности контрактного программирования. Например, инварианты класса наследуются. Как и пост-и предусловия. Это один из аспектов контрактного программирования. Который особенно трудно эмулировать в C++. (Примечание: компилятор Digital Mars C/C++ поддерживает контрактное программирование в качестве расширения языка с 2000 года [9Кроме того. Сложность выноски—публичный метод класса B. Вызывающий публичный метод класса A как часть модификации внутреннего состояния, который. В свою очередь. Вызывает другой метод класса B. Который ошибочно запускает инвариант,—в настоящее время не рассматривается далее.

Если вы хотите еще больше убедить в благости контрактного программирования. Есть несколько важных работ. В том числе [1, 7, 10]. Пока мы будем считать. Что это принято. Однако настоящая полемика заключается в том. Что можно и нужно делать в ответ на нарушения контрактов. Это является основной темой данной статьи и займет большую часть частей 2-4.

Пой со мной (на мотив полиции�с �Де делать делать делать

Ладно, ладно. То, что этот нелепый инициализм—только монгольские горловые певцы могли превратить его в аббревиатуру!—на самом деле представляет собой Отсутствие Доказательств Ошибки. Не Является Доказательством Отсутствия Ошибки В терминах пропозициональной логики обнаружение ошибок/проверка кода-это импликация. А не би-импликация. Другими словами. Если в вашем коде обнаружена ошибка. Ваш код можно назвать ошибочным. Но если в вашем коде не обнаружена ошибка. То нельзя сделать вывод. Что ваш код верен. Мы увидим последствия этого на протяжении всей статьи. Я должен отметить. Что это обязательная покупка в принципе-если вы не примете ее. То будете тратить свое время на чтение.

У компьютеров нет рассуждений более высокого порядка. Они просто выполняют команды. Которые мы им даем на понятном им языке. Следовательно. Вопрос о том. Кто решает. Что представляет собой нарушение для данного фрагмента кода. Чрезвычайно прост: исключительно автор(ы) этого кода. Никто другой не разрабатывал этот код; поэтому никто другой не в состоянии делать заявления о его разработке и воплощать эти заявления в коде в форме принудительного исполнения контрактов. Ни пользователи библиотек. Ни пользователи каких — либо программ. Ни вы, ни я. Ни даже разработчик языка программирования. На котором написан код.

С практической точки зрения некоторые операционные системы могут определить некоторые повреждения памяти. Такие как доступ к незамеченной странице или попытка записи на страницу. Доступную только для чтения. Или что-то еще. Но в целом для скомпилированных языков нет никакой всеобъемлющей помощи со стороны аппаратного обеспечения или чего-либо еще. И даже в таких случаях программист может полагаться на то. Что операционная система поймает нарушение доступа. Чтобы добиться ожидаемого поведения программы—именно так некоторые операционные системы обеспечивают динамическое распределение стека (см. главу 32 книги Imperfect C++ [7]).

Еще один аспект. Который мне нужно затронуть. Прежде чем мы перейдем к мелочам,-это Принцип удаляемости [ 11], который гласит:

Есть две основные причины. Почему это должно быть так. Во-первых, гейзенбергианец: Как мы можем измерить что-то. Если мы его модифицируем? Если бы конструкции контрактного программирования должны были быть частью нормальной функциональной логики. То они должны были бы подлежать обеспечению качества. Требуя исполнения контракта на основе исполнения контракта. И так далее. И так далее. До абсурда. Во-вторых, практический: мы должны быть в состоянии удалить некоторые/все принудительные меры по контракту по соображениям эффективности и/или изменить механизмы проверки/отчетности и реагирования в зависимости от контекста приложения. (Примечание: устранение конкретного правоприменителя не влияет на контрактное поведение. Которое он контролировал. Отмена всех принудительных мер не означает. Что соответствующие контракты были отменены. Равно как и отсутствие принудительных мер не означает. Что соблюдение положений контрактов больше не является обязательным.)

Это будет хлебом с маслом для любого. Кто использовал утверждения. Так как вы будете знать. Что кардинальное правило не должно вносить изменения в предложения утверждений. Потому что это может привести к худшему из всех возможных сценариев отладки: когда код правильно работает в режиме отладки. А не в режиме выпуска. Как только стирается грань между нормальным кодексом и кодексом исполнения контрактов. Все становится запутанным. И ваши клиенты будут недовольны.

Хотя C/C++ не имеют прямой контрактной поддержки. Некоторые языки программирования имеют ее. В частности Eiffel [ 2], который был изобретен Бертраном Мейером, и D [ 12], который был изобретен Уолтером Брайтом. В этих и других языках. Таких как Java и .NET, где используются методы программирования контрактов вне языка. Нарушения контрактов выражаются в виде исключений.

К сожалению, это. Как правило. Способствует размыванию целей контрактов и исключений в умах инженеров. Исторически сложилось так, что это. Как правило. Было гораздо меньше в C и C++. Потому что большинство разработчиков C/C++ использовали утверждения для принудительного исполнения контрактов. Утверждения в C и C++. Как правило. Включаются только в отладочные сборки и исключаются в выпускных сборках по соображениям производительности. Однако по мере того. Как растет понимание сообществом программистов контрактного программирования. Растет интерес к использованию принудительного исполнения контрактов в сборках релизов. Действительно. Как мы увидим в части 4 этой статьи. Есть очень хороший аргумент в пользу того. Чтобы оставить контракты в выпущенном программном обеспечении. Поскольку альтернатива может быть хуже. Иногда катастрофически. Таким образом. Существует вполне реальная опасность того. Что то же самое заблуждение может проникнуть в психику сообщества C++. Поэтому здесь стоит обсудить эти вопросы.

Возникшее исключение обычно представляет собой исключительное условие. С которым может столкнуться допустимая программа и, возможно. Восстановиться после, например. Невозможности открыть сокет. Получить доступ к файлу или подключиться к базе данных. Процесс, в котором такое исключение выбрасывается. Остается допустимым процессом. Независимо от того. Может ли он продолжать выполняться и пытаться повторно открыть/доступ/подключение. Или он выдает сообщение об ошибке и завершает работу корректно. Кроме того, исключения также могут использоваться как часть логики обработки для данного алгоритма/компонента/API. Хотя это происходит реже и. Как правило. Неодобрительно воспринимается в основном [7, 13, 14].

Существуют и другие виды исключений. Из которых процесс обычно не может восстановиться. Но которые не представляют собой недопустимое состояние. Мы можем назвать эти Практически Неустранимые Исключительные Условия. Самый очевидный пример-состояние нехватки памяти. Получив такое исключение. Процесс часто не может сделать ничего другого. Кроме как закрыться. Хотя он все еще находится в действительном состоянии. Это точно так же. Как не иметь возможности открыть файл. Но для того. Что механизмы отчетности и реагирования, вероятно. Захотят использовать память; в таких случаях вы должны рассмотреть возможность использования парашютов памяти [15], который может хорошо работать при некоторых обстоятельствах. Еще одним практически неустранимым исключительным условием является невозможность выделения ресурсов потокоспецифичного хранилища [7, 16, 17] (TSS). Для использования TSS необходимо выделить слоты. Ключами для которых являются хорошо известные значения. Разделяемые между всеми потоками. Которые действуют как индексы в таблицах специфичных для потока данных. Вы получаете свои данные TSS. Указывая ключ. И библиотека TSS разрабатывает слот для вызывающего потока и получает/устанавливает значение для этого слота для вас. TSS лежит в основе многопоточных библиотек-например, errno / GetLastError() per thread — это одно из наиболее простых применений. А исчерпание ключей TSS-это катастрофическое событие. Если вы закончите работу до того. Как построите структуры времени выполнения для своей библиотеки времени выполнения C. То на самом деле нет никакой надежды сделать что-то полезное. И вы очень мало можете сказать об этом.

В отличие от действия исключений. Указывающих на исключительные условия выполнения или как часть логики программы. Нарушение контракта представляет собой обнаружение состояния программы. Явно противоречащего ее дизайну. Как мы увидим в Принципе Безвозвратности (в части 2). При нарушении контракта программа находится, буквально. В недействительном состоянии. И принципе. Нет никаких дальнейших действительных действий. Которые программа может выполнить. Хотя ничего вредного не может произойти. Программа имеет теоретически безграничный потенциал причинения вреда. На практике это. Как правило. Ограничивается текущим процессом или текущей операционной средой. Но теоретически может быть так же плохо. Как послать объявление вечной войны Альфа Центуриям!

Цель контрактов — найти ошибки в программе. Они существуют не для того. Чтобы искать ошибки вне программы. То есть в другой программе или во входных данных этой программы. (Тест в контрактах должен быть съемным и никак не влиять на логическое поведение этой программы. Если произошло изменение, значит. Принудительные меры были использованы неправильно или программа глючит; См.)

Допустим. У нас есть программа. Которая получает дату из своего ввода, возможно. Введенную пользователем. В тот момент. Когда входные данные принимаются программой в качестве даты. Они должны быть подтверждены как приемлемые. То есть такие даты. Как 37 июля 2004 года. Должны быть отклонены с подходящим сообщением и повторной попыткой. Представленной пользователю. Однако, как только дата была подтверждена и принята в логику программы. На некотором уровне внутренняя архитектура программы всегда будет принимать действительные даты. (Не делать этого. А вместо этого кодифицировать ветви с недопустимой датой на каждом уровне приложения было бы очень неэффективно и привело бы к чрезвычайно сложному коду.)

В момент. Когда это предположение сделано. Предварительное условие функции должно отражать это предположение. И было бы уместно поместить принудительное исполнение контракта (напримерassert(IsDateValid(theDate));, или assert(theDate.is_valid());), чтобы защитить это предположение. Это дает уверенность программисту приложения в том. Что на данный момент это действительно действительная дата. Что ничто плохое не проскользнуло мимо валидатора ввода пользователя. И что ничто другое не повредило его в то же время. Сообщение для повторной попытки к пользователю в этот момент не имело бы никакого смысла вообще. Недопустимая дата в этот момент не представляет плохой ввод; она представляет ошибку в программе. Таким образом. Контракты предназначены для поиска ошибок в программе. А не плохих данных. Вводимых в программу.

К сожалению. В использовании исключений в некоторых языках/библиотеках. Как правило. Возникают неясности. Которые могут привести к путанице в том. Что входит в сферу компетенции контрактного программирования. В Java исключения будут создаваться при ошибках переполнения массива. Это исключение является частью спецификации Java. Итак, следующий код является законным Java:

пробовать { int[] array = new int[10]; int i; 

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

Та же ситуация существует и в отношении at()члена C++�s std::basic_stringи , по-видимому. Вызывает довольно большую степень непонимания. Как правило. В путанице между темoperator [](), at()эквивалентны ли и. Давайте посмотрим на at()подпись:

const_reference at(size_type index) const; ссылка на(индекс size_type); 

Стандарт C++ [ 18] гласит следующее:

Требуется: index
Throws: out_of_range, if index >= size()
Returns: operator [](index)

С точки зрения контрактного программирования это немного вводит в заблуждение. Поскольку часть index . Это не так. Действительно. Предварительное условие для at()пусто. То есть сумма всех возможных значений для index[19]:

at()Предварительное условие: s: (пусто)

Постусловие-это то место. Где находится интерес. Поскольку оно утверждает:

at()�s Postcondition: возвращает ссылку на index� — й элемент if index . В противном случае выбрасывает out_of_range.

Другими словами. Если indexон находится в пределах диапазона. То возвращает ссылку на соответствующий элемент. В противном случае он генерирует исключение. Все это не слишком удивительно. Теперь рассмотрим. Чем это отличается от operator []()метода(ов):

 const_reference operator [](индекс size_type) const; ссылочный оператор [](индекс size_type); 

Стандарт [ 18] гласит::

Возвращает: Если index возвращает data()[index]. В противном случае. Если index == size()constверсия возвращается charT(). В противном случае поведение не определено.

Это совершенно другой котелок с рыбой. Если мы запросим действительный индекс, то получим обратно ссылку на соответствующий элемент, как и в случае с at(). (Обратите внимание , что constверсия определяет допустимый диапазон, который должен быть[0, size() + 1), тогда как в constпротивном случае это так [0, size()). Поди разберись! [ 20]) Однако. Если мы не получим правильный индекс. Поведение будет неопределенным.

Termin(ologic)al interlude

Вот простое правило для тех . Кто запутался в том. Что диктует язык: стандарт C++ [18] вообще не затрагивает проблему принудительного исполнения контрактов, хотя и делает приемлемую работу по описанию контрактного поведения (несмотря на неоднозначный язык. Такой как basic_stringat()«Требует»).

Поэтому вы не можете говорить о какой-либо части стандартной библиотеки как о принудительном исполнении контракта. Поскольку она всегда ссылается только на неопределенное поведение. Некоторые реализации. Например Metrowerks. Используют утверждения и. Следовательно. Можно сказать. Что они обеспечивают соблюдение контрактов. Но стандарт просто оставляет такие вещи на усмотрение разработчика. Если вы пишете библиотеки расширений STL—например. Boost [ 21]. STLSoft [ 22]—вы можете интерпретировать каждое

Существуют явные различия между контрактом на at()и.operator []() Контракт на изменяемую (не- const) версию operator []()заключается в следующем:

operator []()index operator []()Предварительное условие: s: Постусловие �s: возвращает ссылку на элемент index�th.

Кроме запутанных пользователей и перегретого стандартного строкового класса [ 23], каковы последствия этих различий? Просто это означает. Что поддерживаются различные парадигмы доступа к элементам. Нормальное манипулирование массивами в C и C++. Через знание диапазона. Поддерживаетсяoperator []():

int main() { std::string s(�Что-то или что-то ещеfor(std::string::size_type i = 0; i) { std::cout 

А метод catch-out-of-bounds. Как показано в предыдущем примере Java. Поддерживаетсяat():

 int main() { std::string s(�Что-то или что-то ещепопробуйте { for(std::string::size_type i = 0; ; ++i) 

Важно понимать. Что оба они представляют собой полностью валидные программы. В которых клиентский код соблюдает контракты соответствующих std::basic_stringиспользуемых методов. Повторяю, указание индекса out-of-bound для at()не является нарушением контракта. В то время как это. Безусловно. Так operator [](). Это разграничение между выбрасыванием исключений и неопределенным поведением (т. Е. Нарушением контракта) существует в равной степени вне стандарта. Рассмотрим сопоставление STL для библиотеки Open-RJ [24]. recordКлассы предоставляют an operator [](char_type const *fieldName), который выбрасывает std::out_of_rangeif fieldName не соответствует существующему полю для этой записи в базе данных. Теперь это. Конечно. Не тот случай. Когда запрос поля (по имени). Которое не существует. Является недопустимым кодом. Он обеспечивает простой и элегантный стиль в клиентском коде:

openrj::stl::file_database db(пробовать { for(openrj::stl::database::iterator b = db.begin (); . ..) / / перечисление записей бд { openrj::stl::запись r(*b); std::cout 

recordКласс также предоставляет operator [](size_type index)метод. Для которого индекс out of bounds представляет собой нарушение контракта. Таким образом. Следующий код представляет собой плохо сформированную программу:

 . . . openrj::cpp::Record record(*b); for(size_t i = 0; ; ++i) 

В то время как первый является вполне допустимым кодом и является разумным инструментом для проверки правильности баз данных домашних Record::operator []()животных—с помощью брошенного исключения в случае. Если запись не содержит поля данного имени,—последний плохо сформирован и вызовет у вас горе (т. Е. Сбой).

И если вы все еще скептически относитесь к тому. Что исключения могут быть частью контракта функции. Рассмотрите случай operator new()функции. Если бы выбрасывание экземпляра bad_alloc(или чего—то производного от bad_allocнего ) не входило в его контракт. Это означало бы. Что исчерпание памяти—условие времени выполнения. В значительной степени находящееся вне контроля разработчика программы. — было бы нарушением контракта. То есть недвусмысленным утверждением о противоречии дизайна! Теперь это сделало бы написание хорошего программного обеспечения чем-то вроде вызова …

Я углублюсь в последствия неудачи и представлю Принцип неустранимости вместе с некоторыми примерами. Которые помогут устранить возражения. Которые были выдвинуты против него. Я закончу , изучив Ошибочность Нарушения Восстанавливаемого предварительногоусловия. Которое прекрасно приведет к Части 3. Спасибо Бьорну Карлссону. Кристоферу Диггинсу. Чаку Эллисону. Джону Торджо. Торстену Оттосену и Уолтеру Брайту за продолжительное и бодрящее обсуждение этого и других вопросов контрактного программирования в октябре 2004 года. Были слезы, страхи. Насмешки и несколько насмешливых взглядов—к сожалению. Пива не было,—но я думаю. Что все это к лучшему. Особая мысль о симпатии к Уолтеру. Чьи взгляды наиболее близки к моим собственным—какое безумное печенье!—и несколько гномических наблюдений которых были включены в текст в нескольких местах

Спасибо также членам группы новостей D (news://news.digitalmars.com/d) за аналогичную стимулирующую дискуссию в апреле 2005 года. В частности Бену Хинклу. Дереку Парнеллу. Джорджу Сбреде и Риган Хит. Вы заставили меня очень усердно работать. Чтобы заполнить пробелы в Принципе Безвозвратности. Которые раньше удерживались только инстинктом и скрещенными пальцами. Особая благодарность Шону Келли за стимулирование мыслительного процесса. Приведшего к Ошибочности Восстановимого Нарушения Предварительного условия (часть 2).

Спасибо также следующим рецензентам: Эндрю Ханту. Бьорну Карлссону. Кристоферу Диггинсу. Кевлину Хенни. Невину Либеру. Шону Келли. Торстену Оттосену. Уолтеру Брайту. Особая благодарность Крису. Чья сухость и строгость в рецензии оказались столь ценным комплиментом моей интуиции и многословию, и Кевлину. Чья красноречивая критика мягко заставила бы задуматься самого бесспорного евангелиста. И я также хотел бы поблагодарить моего редактора Чака Эллисона за действия. Выходящие за рамки служебного долга и помогающие мне приготовить эту еду левиафана в удобоваримые порции.

Несмотря на всю полученную помощь. Любые ошибки. Плохие шутки и плохие суждения-это мои собственные.

Спасибо за чтение,
Мэтью Уилсон

  1. Объектно-ориентированное построение программногообеспечения . Бертран Мейер. Прентис Холл, 1997
  2. Язык программирования Eiffel (http://www.eiffel.com)
  3. Предложение добавить контрактное программирование в C++, Лоуренс Кроул и Торстен Оттосен, WG21/N1773 и J16/05-0033, 4 марта 2005 г. ( http://www.open — std.org/JTC1/SC22/WG21/docs/papers/2005/n1773.html)
  4. Электронная переписка с Кевлином Хенни. Июнь 2005 г.
  5. Дизайн по контракту; Беседа с Бертраном Мейером, Часть II, Билл Веннерс, Artima, 8 декабря 2003 г. ( http://www.artima.com/intv/contracts.html)
  6. Электронная переписка с Кристофером Диггинсом. Май 2005 г.
  7. Несовершенный C++, Мэтью Уилсон, Аддисон-Уэсли, 2004 (http://imperfectcplusplus.com/)
  8. Сортировка, Кевлин Хенни, советник по разработке приложений, июль-август 2003 года.
  9. Digital Mars C/C++ compiler Contract Programming support ( www.digitalmars.com/ctg/contract.html).
  10. Прагматичный программист, Дэйв Хант и Энди Томас. Аддисон-Уэсли, 2000
  11. Беседа по электронной почте с Кристофером Диггинсом. Октябрь 2004 г.
  12. D-это новый системный язык программирования. Созданный Уолтером Брайтом (из Digital Mars; http://www.digitalmars.com/), который объединяет многие лучшие функции C. C++ и других продвинутых языков. Включая поддержку контрактного программирования.
  13. Практика программирования, Керниган и Пайк. Аддисон-Уэсли, 1999
  14. Искусство программирования UNIX, Eric Raymond. Addison-Wesley, 2003
  15. Прыжки с вершины парашютов, Мэтью Уилсон. Блог 18 апреля 2005 ( http://www.artima.com/weblogs/viewpost.jsp?thread=104862)
  16. Программирование С Потоками POSIX, David R. Butenhof. Addison-Wesley, 1997
  17. Advanced Windows, Джеффри Рихтер. Microsoft Press, 1997
  18. Стандарт C++, ISO/IEC 14882:98
  19. Торстен Оттосен предложил в переписке по электронной почте в мае 2005 года альтернативное представление для предварительного условия. Которое допускает все возможные значения какtrue, а не пустое условие. Это прекрасно уравновесило бы теоретическое условие falseдля функции. Не имеющей выполнимых предварительных условий. Мы все сталкивались с парочкой таких в наших путешествиях. 😉
  20. Очевидно, причина в том. Что возвратconstнемутабельной ссылки ( ) к нулевому терминатору безвреден. Тогда как возврат изменяемой (неconst) ссылки-это совсем не так. Вопрос о том. Стоит ли эта несогласованность скромного увеличения неизменяемой гибкости. Выходит за рамки данной статьи.
  21. Boost-это организация с открытым исходным кодом. Специализирующаяся на разработке библиотек. Интегрирующихся со стандартной библиотекой C++. http://boost.org/. В нем насчитываются тысячи членов. В том числе многие из самых известных представителей сообщества программистов на С++.
  22. STLSoft-это организация с открытым исходным кодом. В центре внимания которой находится разработка надежного, легкого. Простого в использовании. Кросс-платформенного STL — совместимого программного обеспечения. http://stlsoft.org/. У него меньше членов. Чем у Boost.
  23. Кевлин Хенни, Консультант По Разработке Приложений, Том 6, Номер 6, Июль-Август 2002 Года.
  24. Open-RJ-это независимая от платформы структурированная библиотека чтения файлов с открытым исходным кодом для формата Record JAR. Он доступен по адресу http://openrj.org/.

Ядерный реактор и космический зонд. Часть 1
Мэтью Уилсон
©2004-2006.

Отвечай!

Есть мнение? Читатели уже разместили 18 комментариев по поводу этой статьи. Почему бы не добавить свою?

Об авторе

Мэтью Уилсон-консультант по разработке программного обеспечения. Редактор журнала пользователя C/C++и создатель библиотек STLSoft (http://stlsoft.org/) . Он является автором книги Imperfect C++ (Addison-Wesley, 2004) и в настоящее время работает над своими следующими двумя книгами. Одна из которых не посвящена C++. Аппетит Мэтью к задачам программирования сравним только с его аппетитом к шоколаду; он держит их в узде. Сочиняя статьи и катаясь на велосипеде (не всегда в одно и то же время). С ним можно связаться по телефону http://imperfectcplusplus.com/.