Проверка инвариантов в С++

Существуют ли установленные шаблоны для проверки инвариантов классов в С++?

В идеале, инварианты будут автоматически проверяться в начале и в конце каждой публичной функции-члена. Насколько мне известно, C с классами предоставлял специальные функции before и after, но, к сожалению, дизайн по контракту в то время был не очень популярен, и никто, кроме Бьярна, не использовал эту функцию, поэтому он удалил ее.

Конечно, ручная вставка вызовов check_invariants() в начале и в конце каждой публичной функции-члена утомительна и подвержена ошибкам. Поскольку RAII является оружием выбора для устранения исключений, я придумал следующую схему определения проверки инвариантности как первой локальной переменной, и эта проверка инвариантности проверяет инварианты как при построении, так и времени разрушения:

template <typename T>
class invariants_checker
{
    const T* p;

public:

    invariants_checker(const T* p) : p(p)
    {
        p->check_invariants();
    }

    ~invariants_checker()
    {
        p->check_invariants();
    }
};

void Foo::bar()
{
    // class invariants checked by construction of _
    invariants_checker<Foo> _(this);

    // ... mutate the object

    // class invariants checked by destruction of _
}

Вопрос № 0: Я полагаю, что невозможно объявить неназванную локальную переменную?:)

Нам все равно придется называть check_invariants() вручную в конце конструктора Foo и в начале деструктора Foo. Однако многие тела конструктора и тела деструктора пустые. В этом случае мы могли бы использовать invariants_checker как последний элемент?

#include <string>
#include <stdexcept>

class Foo
{
    std::string str;
    std::string::size_type cached_length;
    invariants_checker<Foo> _;

public:

    Foo(const std::string& str)
    : str(str), cached_length(str.length()), _(this) {}

    void check_invariants() const
    {
        if (str.length() != cached_length)
            throw std::logic_error("wrong cached length");
    }

    // ...
};

Вопрос №1: допустимо ли передать this в конструктор invariants_checker, который сразу вызывает check_invariants через этот указатель, хотя объект Foo все еще находится в разработке?

Вопрос №2: Вы видите какие-либо другие проблемы с этим подходом? Можете ли вы его улучшить?

Вопрос № 3: Является ли этот подход новым или общеизвестным? Доступны ли лучшие решения?

Ответы

Ответ 1

Ответ # 0: У вас могут быть неназванные локальные переменные, но вы отказываетесь от контроля над временем жизни объекта, а вся цель объекта - потому, что у вас есть хорошая идея, когда она выходит за рамки. Вы можете использовать

void Foo::bar()
{
    invariants_checker<Foo>(this); // goes out of scope at the semicolon
    new invariants_checker<Foo>(this); // the constructed object is never destructed
    // ...
}

но это не то, что вам нужно.

Ответ № 1: Нет, я считаю, что это недействительно. Объект, на который ссылается this, только полностью сконструирован (и, таким образом, начинает существовать), когда конструктор закончил. Здесь вы играете в опасную игру.

Ответ # 2 и # 3: Этот подход не является новым, простым запросом google, например. "проверка инвариантов С++-шаблона" даст много хитов по этой теме. В частности, это решение может быть дополнительно улучшено, если вы не против перегрузки оператора ->, например:

template <typename T>
class invariants_checker {
public:
  class ProxyObject {
  public:
    ProxyObject(T* x) : m(x) { m->check_invariants(); }
    ~ProxyObject() { m->check_invariants(); }
    T* operator->() { return m; }
    const T* operator->() const { return m; }
  private:
    T* m;
  };

invariants_checker(T* x) : m(x) { }

ProxyObject operator->() { return m; } 
const ProxyObject operator->() const { return m; }

private:
   T* m;
};

Идея заключается в том, что в течение вызова функции-члена вы создаете анонимный прокси-объект, который выполняет проверку в своем конструкторе и деструкторе. Вы можете использовать приведенный выше шаблон следующим образом:

void f() {
  Foo f;
  invariants_checker<Foo> g( &f );
  g->bar(); // this constructs and destructs the ProxyObject, which does the checking
}

Ответ 2

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

Я думаю, что это слишком много; Я проверяю инварианты разумно. Элементами данных вашего класса являются private (справа?), Поэтому только его функции-члены могут изменять данные memebers и, следовательно, недействительны инварианты. Таким образом, вы можете избежать проверки инварианта сразу после изменения элемента данных, который участвует в этом инварианте.

Ответ 3

Вопрос № 0: Я полагаю, что невозможно объявить неназванную локальную переменную?:)

Обычно вы можете взломать что-то с помощью макросов и __LINE__, но если вы просто выберите довольно странное имя, оно уже должно быть выполнено, поскольку у вас не должно быть более одного (напрямую) в той же области. Это

class invariants_checker {};

template<class T>
class invariants_checker_impl : public invariants_checker {
public:
    invariants_checker_impl(T* that) : that_(that) {that_->check_invariants();}
    ~invariants_checker_impl()                     {that_->check_invariants();}
private:
    T* that_;
};

template<class T>
inline invariants_checker_impl<T> get_invariant_checker(T* that)
{return invariants_checker_impl<T>(that);}

#define CHECK_INVARIANTS const invariants_checker& 
   my_fancy_invariants_checker_object_ = get_invariant_checker(this)

работает для меня.

Вопрос №1: Действительно ли передать this в конструктор invariants_checker, который сразу вызывает check_invariants через этот указатель, хотя объект Foo все еще находится в разработке?

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

Вопрос №2: Вы видите какие-либо другие проблемы с этим подходом? Можете ли вы его улучшить?

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

Вопрос № 3: Является ли этот подход новым или общеизвестным? Доступны ли лучшие решения?

Я не видел такого подхода, но, учитывая ваше описание before() и after(), я сразу подумал о том же решении.

Я думаю, что у Страуструпа была статья много (~ 15?) лет назад, где он описал класс дескриптора перегрузки operator->(), чтобы вернуть прокси. Это могло бы в его ctor и dtor выполнять до и после действия, не обращая внимания на методы, вызываемые через него.

Изменить: Я вижу, что Фририх добавил ответ, содержащийся в нем. Конечно, если ваш класс уже не должен использоваться через такой дескриптор, это бремя для пользователей вашего класса. (IOW: Это не сработает.)

Ответ 4

# 0: Нет, но все может быть немного лучше с макросом (если вы в порядке с этим)

# 1: Нет, но это зависит. Вы не можете делать ничего, что могло бы привести к тому, что это будет разыменовано перед телом (которое было бы, но только раньше, чтобы оно могло работать). Это означает, что вы можете сохранить это, но не получить доступ к полям или виртуальным функциям. Вызов check_invariants() не подходит, если он виртуальный. Я думаю, что это сработает для большинства реализаций, но не гарантируется работа.

# 2: Я думаю, это будет утомительно, и не стоит этого. Это был мой опыт с инвариантной проверкой. Я предпочитаю модульные тесты.

# 3: Я видел это. Мне кажется правильным способом, если вы собираетесь это сделать.

Ответ 5

модульное тестирование - лучшая альтернатива, которая приводит к меньшему коду с лучшей производительностью

Ответ 6

Я ясно вижу, что ваш деструктор вызывает функцию, которая часто бросает, что нет-нет в С++, не так ли?