проверка инвариантов в C ++ - PullRequest
15 голосов
/ 19 января 2011

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

В идеале, инварианты должны автоматически проверяться в начале и в конце каждой общедоступной функции-члена.Насколько я знаю, в 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: Этот подход новый или известный?Есть ли лучшие решения?

Ответы [ 6 ]

11 голосов
/ 19 января 2011

Ответ # 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 для напр. «Проверка инвариантов шаблона C ++» приведет к большому количеству обращений по этой теме. В частности, это решение может быть улучшено, если вы не возражаете перегрузить оператор ->, например:

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 голосов
/ 19 января 2011

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

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

1 голос
/ 19 января 2011

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

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

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

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

1 голос
/ 19 января 2011

Вопрос № 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: это не сработает.)

0 голосов
/ 19 января 2011

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

0 голосов
/ 19 января 2011

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

...