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

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

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

Вопрос №3: Этот подход новый или хорошо известный? Существуют ли лучшие решения?

6 ответов


ответ #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" даст много хитов по этой теме. В частности, это решение может быть улучшено, если вы не против перегрузки -> оператор как это:

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
}

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

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


вопрос #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 объект все еще строится?

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

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

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

Вопрос #3: это новые или хорошо известные? Существуют ли лучшие решения?

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

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

Edit: я вижу, что Frerich имеет добавлено ответ fleshing это. Конечно, если ваш класс уже не должен использоваться через такой дескриптор, это бремя для пользователей вашего класса. (IOW: это не сработает.)


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

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

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

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


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


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