检查 C++ 中的不变量

发布于 2024-10-12 18:28:50 字数 1691 浏览 13 评论 0原文

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:我想有没有办法声明一个未命名的局部变量? :)

我们仍然需要在 Foo 构造函数的末尾和 Foo 析构函数的开头手动调用 check_invariants()。然而,许多构造函数体和析构函数体是空的。在这种情况下,我们可以使用 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:这种方法是新的还是众所周知的?有更好的解决方案吗?

Are there any established patterns for checking class invariants in C++?

Ideally, the invariants would be automatically checked at the beginning and at the end of each public member function. As far as I know, C with classes provided special before and after member functions, but unfortunately, design by contract wasn't quite popular at the time and nobody except Bjarne used that feature, so he removed it.

Of course, manually inserting check_invariants() calls at the beginning and at the end of each public member function is tedious and error-prone. Since RAII is the weapon of choice to deal with exceptions, I came up with the following scheme of defining an invariance checker as the first local variable, and that invariance checker checks the invariants both at construction and destruction time:

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 _
}

Question #0: I suppose there is no way to declare an unnamed local variable? :)

We would still have to call check_invariants() manually at the end of the Foo constructor and at the beginning of the Foo destructor. However, many constructor bodies and destructor bodies are empty. In that case, could we use an invariants_checker as the last member?

#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");
    }

    // ...
};

Question #1: Is it valid to pass this to the invariants_checker constructor which immediately calls check_invariants via that pointer, even though the Foo object is still under construction?

Question #2: Do you see any other problems with this approach? Can you improve it?

Question #3: Is this approach new or well-known? Are there better solutions available?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

孤檠 2024-10-19 18:28:50

答案#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
}

Answer #0: You can have unnamed local variables, but you give up control over the life time of the object - and the whole point of the object is because you have a good idea when it goes out of scope. You can use

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
    // ...
}

but neither is what you want.

Answer #1: No, I believe it's not valid. The object referenced by this is only fully constructed (and thus starts to exist) when the constructor finished. You're playing a dangerous game here.

Answer #2 & #3: This approach is not new, a simple google query for e.g. "check invariants C++ template" will yield a lot of hits on this topic. In particular, this solution can be improved further if you don't mind overloading the -> operator, like this:

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;
};

The idea is that for the duration of a member function call, you create an anonymous proxy object which performs the check in its constructor and destructor. You can use the above template like this:

void f() {
  Foo f;
  invariants_checker<Foo> g( &f );
  g->bar(); // this constructs and destructs the ProxyObject, which does the checking
}
始终不够爱げ你 2024-10-19 18:28:50

理想情况下,会在每个公共成员函数的开头和结尾自动检查不变量

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

Ideally, the invariants would be automatically checked at the beginning and at the end of each public member function

I think this is overkill; I instead check invariants judiciously. The data members of your class are private (right?), so only its member functions can change the data memebers and therefore invalidate invariants. So you can get away with checking an invariant just after a change to a data member that particiaptes in that invariant.

空城缀染半城烟沙 2024-10-19 18:28:50

问题#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,即使 < code>Foo 对象仍在构建中?

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

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

参见#2。参加一个中等规模的课程,加上两打开发人员五年的扩展和错误修复,我认为至少有一次搞砸的可能性约为 98%。
您可以通过向数据成员添加大声注释来在一定程度上缓解这种情况。仍然。

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

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

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

编辑:我看到 Frerich 添加了

Question #0: I suppose there is no way to declare an unnamed local variable? :)

You can usually whip up something using macros and __LINE__, but if you just pick a strange enough name, it should already do, since you shouldn't have more than one (directly) in the same scope. This

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)

works for me.

Question #1: Is it valid to pass this to the invariants_checker constructor which immediately calls check_invariants via that pointer, even though the Foo object is still under construction?

I'm not sure whether it invokes UB technical. In practice it would certainly be safe to do so - where it not for the fact that, in practice, a class member that has to be declared at a specific position in relation to other class members is going to be a problem sooner or later.

Question #2: Do you see any other problems with this approach? Can you improve it?

See #2. Take a moderately sized class, add half a decade of extending and bug-fixing by two dozen developers, and I consider the chances to mess this up at at least once at about 98%.
You can somewhat mitigate this by adding a shouting comment to the data member. Still.

Question #3: Is this approach new or well-known? Are there better solutions available?

I hadn't seen this approach, but given your description of before() and after() I immediately thought of the same solution.

I think Stroustrup had an article many (~15?) years ago, where he described a handle class overloading operator->() to return a proxy. This could then, in its ctor and dtor, perform before- and after-actions while being oblivious to the methods being invoked through it.

Edit: I see that Frerich has added an answer fleshing this out. Of course, unless your class already needs to be used through such a handle, this is a burden onto your class' users. (IOW: It won't work.)

×纯※雪 2024-10-19 18:28:50

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

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

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

#3:我已经看到了。如果你打算这样做的话,这对我来说似乎是正确的方法。

#0: No, but things could be slightly better with a macro (if you're ok with that)

#1: No, but it depends. You cannot do anything that would cause this to be dereferenced in before the body (which yours would, but just before, so it could work). This means that you can store this, but not access fields or virtual functions. Calling check_invariants() is not ok if it's virtual. I think it would work for most implementations, but not guaranteed to work.

#2: I think it will be tedious, and not worth it. This have been my experience with invariant checking. I prefer unit tests.

#3: I've seen it. It seems like the right way to me if you're going to do it.

潦草背影 2024-10-19 18:28:50

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

unit testing is better alternative that leads to smaller code with better performance

ゞ记忆︶ㄣ 2024-10-19 18:28:50

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

I clearly see the issue that your destructor is calling a function that will often throw, that's a no-no in C++ isn't it?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文