Все статьи / Программирование по контракту в языке C++


Design by Contract — принцип программирования. Его иногда ошибочно считают технологией и стремятся делать строго-как-в-учебниках, но это непрактичный подход.

Контракты функций

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

// файл GameMath.h
#pragma once

struct Math
{
    Math() = delete;
    static int Random(int a, int b);
}
// файл GameMath.cpp
#include "GameMath.h"

int Math::Random(int a, int b)
{
    return a + rand() % (a - b);
}
// файл, использующий структуру Math
#include "GameMath.h"

void foo()
{
    const int MIN_SPEED = 1;
    const int MAX_SPEED = 4;
    int sparkleSpeed = Math::Random(MIN_SPEED, MAX_SPEED);
    // ...
}

Читатель, анализирующий код функции foo(), будет задаваться вопросами:

  • Что означают a и b, почему туда передаются MIN_SPEED и MAX_SPEED?
  • Могу ли я через Math::Random(1, 10) получить число 10?
  • Что вернёт Math::Random(0, -7)?

В итоге читателю для анализа foo() придётся изучить код других функций. Проблема легко устраняется, если ввести:

  • интуитивно понятные названия параметров
  • контракт в виде комментария в заголовочном файле
  • проверку соблюдения контракта с помощью макроса assert
// файл GameMath.h
#pragma once

struct Math
{
    Math() = delete;
    // Returns value in range [min, max].
    // 'max' must be more than 'min'.
    static int Math::Random(int min, int max);
}
// файл GameMath.cpp
int Math::Random(int min, int max)
{
    assert(min < max);
    return min + rand() % (max - min + 1);
}

Инварианты объектов и структур данных

Проверка инвариантов, постусловий и предусловий у объектов изучается подробнее в курсе ООП. Это не мешает использовать инварианты, описанные в документации STL, например, на cppreference.com.

Примеры инвариантов в STL:

  • у std::vector vec; всегда 0 <= vec.size() <= vec.capacity()
  • у std::string str; метод str.c_str() всегда возвращает указатель на последовательность символов с нулевым символом '\0' на конце.

Примеры предусловий в STL:

  • у std::vector vec; при запросе по индексу vec[index] индекс должен быть в диапазоне [0, size)

Примеры постусловий в STL:

  • после изменения std::string str; все инварианты сохраняются

Немного теории

Контракты — часть формальных методов проверки корректности программ.

Опытные разработчики могут использовать другие реализации идеи контрактов (но перечисленные ниже способы не рекомендуются для новичков):

  • для объектно-ориентированного кода: модульное тестирование (unit testing) и mock-объекты с контрактами
  • для статического анализа кода: контракты из стандарта C++ 2017
  • для метапрограммирования: static_assert, концепты из стандарта C++ 2017
  • грамотное создание API с учётом ООП, как пример — сокрытие данных с private/public и идиомой PIMPL
  • грамотное создание API с учётом ФП, как пример — optional и другие алгебраические типы данных

Читать далее