16

是否有任何已建立的模式来检查 C++ 中的类不变量?

理想情况下,将在每个公共成员函数的开始和结束时自动检查不变量。据我所知,带有类的 C 语言提供了特殊beforeafter成员函数,但不幸的是,当时契约式设计并不是很流行,除了 Bjarne 之外没有人使用该功能,因此他将其删除。

当然,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但是,许多构造函数体和析构函数体是空的。在那种情况下,我们可以使用 aninvariants_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:即使对象仍在构造中,传递thisinvariants_checker立即通过该指针调用的构造函数是否有效?check_invariantsFoo

问题 #2:您认为这种方法还有其他问题吗?你能改进它吗?

问题 3:这种方法是新的还是众所周知的?有没有更好的解决方案?

4

6 回答 6

12

答案#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:这种方法并不新鲜,例如“检查不变量 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
}
于 2011-01-19T14:16:47.927 回答
2

理想情况下,将在每个公共成员函数的开始和结束时自动检查不变量

我认为这是矫枉过正。相反,我会明智地检查不变量。您的类的数据成员是private(对吗?),因此只有其成员函数才能更改数据成员,从而使不变量无效。因此,您可以在更改参与该不变量的数据成员之后立即检查该不变量。

于 2011-01-19T14:27:09.793 回答
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:即使对象仍在构造中,传递thisinvariants_checker立即通过该指针调用的构造函数是否有效?check_invariantsFoo

我不确定它是否调用了UB技术。在实践中这样做肯定是安全的——这并不是因为在实践中,必须在与其他类成员相关的特定位置声明的类成员迟早会成为问题。

问题 #2:您认为这种方法还有其他问题吗?你能改进它吗?

见#2。参加一个中等规模的课程,加上两打开发人员五年的扩展和错误修复,我认为至少有 98% 的机会把这件事搞砸。
您可以通过向数据成员添加大喊大叫的评论来缓解这种情况。仍然。

问题 3:这种方法是新的还是众所周知的?有没有更好的解决方案?

我没有见过这种方法,但是鉴于您的描述,before()after()立即想到了相同的解决方案。

我认为 Stroustrup 很多年前(约 15 年?)有一篇文章,他在其中描述了句柄类重载operator->()以返回代理。然后,这可以在其 ctor 和 dtor 中执行前后操作,同时忽略通过它调用的方法。

编辑:我看到 Frerich 添加了一个答案来充实这一点。当然,除非您的类已经需要通过这样的句柄使用,否则这对您的类的用户来说是一种负担。(IOW:这行不通。)

于 2011-01-19T14:03:45.493 回答
1

#0:不,但是使用宏可能会稍微好一些(如果你同意的话)

#1:不,但这取决于。你不能做任何会导致它在身体之前被取消引用的事情(你会这样做,但就在之前,所以它可以工作)。这意味着您可以存储它,但不能访问字段或虚拟函数。如果它是虚拟的,则调用 check_invariants() 是不行的。我认为它适用于大多数实现,但不能保证有效。

#2:我认为这会很乏味,而且不值得。这是我对不变检查的经验。我更喜欢单元测试。

#3:我见过。如果您要这样做,这对我来说似乎是正确的方法。

于 2011-01-19T14:32:40.460 回答
0

单元测试是更好的选择,它可以使代码更小,性能更好

于 2011-01-19T14:01:22.740 回答
0

我清楚地看到了你的析构函数正在调用一个经常抛出的函数的问题,这在 C++ 中是禁止的,不是吗?

于 2011-01-19T14:59:40.930 回答