13

Существуют ли установленные шаблоны для проверки инвариантов классов в C++?проверка инвариантов в 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: Этот подход является новым или общеизвестным? Есть ли лучшие решения?

+0

Вы не можете использовать 'this' в списке инициализаторов. Вы можете использовать его, однако, в теле конструктора. – Benoit

+0

@Benoit: Что вы имеете в виду с * не может *? Это строго запрещено? Вызывает ли это неопределенное поведение? – fredoverflow

+1

Thorsten Ottesen (я думаю, что это было) было предложение по дизайну по контракту. В первом раунде он не вышел из-под контроля, из-за трудности принятия решения о внутреннем и внешнем вызове (вы можете временно нарушить инвариант для внутреннего вызова). Но он все еще может появиться. Не знаю, активно ли он работает. –

ответ

10

Ответ # 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 
} 
+1

+1 для представления идеи «ProxyObject». очень хорошо! – Nawaz

+1

0) не является неназванной переменной, а скорее временным объектом. Нет объявленной переменной, но только один оператор, создающий временное и ничего не с ним делать. +1 для последнего блока и предложение перегрузки 'operator->' ... причудливого маленького зверя. (Еще одно подобное использование показано в «Modern C++ design», чтобы обеспечить блокирующий интеллектуальный указатель. Предложения о добавлении смарт-указателя блокировки к стандарту были отклонены на том основании, что зернистость блокировок в большинстве случаев не будет лучшим выбором) –

+1

@David: Тогда в чем разница между неназванной переменной (AKA анонимным значением) и временным объектом? –

0

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

+0

Итак, как получилось, что D имеет дизайн по контракту * и * единичное тестирование, встроенное в язык? :) – fredoverflow

+0

@Fred: да, вы правы, иногда проверка инвариантов - хорошая идея, но для такого систематического подхода я бы предпочел использовать модульное тестирование –

1

Вопрос # 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 выполнять до и после действия, не обращая внимания на методы, вызываемые через него.

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

2

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

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

1

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

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

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

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

0

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

+0

Абсолютно - потому что деструкторы вызываются при разматывании стека во время исключения. Невозможно узнать разницу. Тем не менее, я ожидаю, что инвариантная проверка будет присутствовать только во время тестирования, а не в производственном коде, поэтому может быть и так. –

+1

@ Lou: Если происходит проверка инварианта, это означает, что в коде есть ошибка программирования, и класс находится в состоянии, в котором он никогда не должен находиться. В этом случае все ставки отключены, и программа должна просто завершиться. Независимо от того, обнаружено ли это в dtor __ совершенно не важно, когда программа все равно прекратится – sbi