14

类层次结构的一个非常常见的错误是将基类中的方法指定为虚拟方法,以便继承链中的所有覆盖都做一些工作,而忘记将调用传播到基实现。

示例场景

class Container
{
public:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Nothing to do here
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Set some property of pObject and pass on.
    Container::PrepareForInsertion(pObject);
  }
};

class MoreSpecializedContainer : public SpecializedContainer
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Oops, forgot to propagate!
  }
};

我的问题是:是否有一种好的方法/模式来确保始终在调用链的末尾调用基本实现?

我知道有两种方法可以做到这一点。

方法一

您可以将成员变量用作标志,在虚方法的基本实现中将其设置为正确的值,并在调用后检查其值。这需要使用公共的非虚拟方法作为客户端的接口,并使虚拟方法受到保护(这实际上是一件好事),但它需要专门为此目的使用成员变量(这需要如果虚方法必须是 const 则可变)。

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    m_callChainCorrect = false;
    PrepareForInsertionImpl(pObject);
    assert(m_callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    m_callChainCorrect = true;
  }

private:
  bool m_callChainCorrect;
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject);
  }
};

方法二

另一种方法是用不透明的“cookie”参数替换成员变量并做同样的事情:

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    bool callChainCorrect = false;
    PrepareForInsertionImpl(pObject, &callChainCorrect);
    assert(callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    *reinrepret_cast<bool*>(pCookie) = true;
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject, pCookie);
  }
};

在我看来,这种方法不如第一种,但它确实避免了使用专用成员变量。

还有哪些其他可能性?

4

7 回答 7

22

You've come up with some clever ways to do this, at (as you acknowledge) the cost of bloating the class and adding code that addresses not the object's responsibilities but programmer deficiences.

The real answer is not to do this at runtime. This is a programmer error, not a runtime error.

Do it at compile time: use a language construct if the language supports it, or use a Pattern the enforces it (e.g,, Template Method), or make your compilation dependent on tests passing, and set up tests to enforce it.

Or, if failing to propagate causes the derived class to fail, let it fail, with an exception message that informs the author of the derived class that he failed to use the base class correctly.

于 2009-08-13T17:26:10.160 回答
13

What you are looking for is simply the Non-Virtual Interface pattern.

It's similar to what you are doing here, but the base class implementation is guaranteed to be called because it's the only implementation that can be called. It eliminates the clutter that your examples above require. And the call through the base class is automatic, so derived versions don't need to make an explicit call.

Google "Non-Virtual Interface" for details.

Edit: After looking up "Template Method Pattern", I see that it's another name for Non-Virtual Interface. I've never heard it referred to by the name before (I'm not exactly a card-carrying member of the GoF fan club). Personally, I prefer the name Non-Virtual Interface because the name itself actually describes what the pattern is.

Edit Again: Here's the NVI way of doing this:

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    PrepareForInsertionImpl(pObject);

    // If you put some base class implementation code here, then you get
    // the same effect you'd get if the derived class called the base class
    // implementation when it's finished.
    //
    // You can also add implementation code in this function before the call
    // to PrepareForInsertionImpl, if you want.
  }

private:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject) = 0;
};

class SpecializedContainer : public Container
{
private:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // Do something and return to the base class implementation.
  }
};
于 2009-08-13T17:35:04.647 回答
6

当只有一层继承时,您可以使用模板方法模式,其中公共接口是非虚拟的并调用虚拟实现函数。然后基础的逻辑进入确保被调用的公共函数。

如果你有不止一层的继承,并且希望每个类都调用它的基类,那么你仍然可以使用模板方法模式,但是有一个转折点,使虚函数的返回值只能被构造, base所以derived将被强制调用基本实现以返回一个值(在编译时强制执行)。

这并不强制每个类调用其直接基类,它可能会跳过一个级别(我想不出一个好的方法来强制执行)但它确实迫使程序员做出有意识的决定,换句话说它有效反对疏忽而不是恶意。

class base {
protected:
    class remember_to_call_base {
        friend base;
        remember_to_call_base() {} 
    };

    virtual remember_to_call_base do_foo()  { 
        /* do common stuff */ 
        return remember_to_call_base(); 
    }

    remember_to_call_base base_impl_not_needed() { 
        // allow opting out from calling base::do_foo (optional)
        return remember_to_call_base();
    }

public:
    void foo() {
        do_foo();
    }
};

class derived : public base  {

    remember_to_call_base do_foo()  { 
        /* do specific stuff */
        return base::do_foo(); 
    }
};

如果您需要public(non virtual) 函数返回一个值,则内部函数virtual应该返回std::pair<return-type , remember_to_call_base>


注意事项:

  1. remember_to_call_base有一个显式构造函数声明为私有,因此只有它friend(在这种情况下base)可以创建此类的新实例。
  2. remember_to_call_base 没有明确定义的复制构造函数,因此编译器将创建一个具有public可访问性的复制构造函数,允许从base实现中按值返回它。
  3. remember_to_call_base在 的protected部分中声明base,如果它在该private部分derived中将根本无法引用它。
于 2009-08-13T17:59:05.587 回答
4

A completely different approach would be to register functors. Derived classes would register some function (or member function) with the base class while in the derived class constructor. When the actual function is called by the client it is a base class function which then iterates through the registered functions. This scales to many levels of inheritance, each derived class only has to be concerned with its own function.

于 2009-08-13T17:39:20.237 回答
0

Look at the template method pattern. (The basic idea is that you don't have to call the base class method anymore.)

于 2009-08-13T17:25:45.170 回答
0

One way out is to not use virtual methods at all, but instead to allow the user to register callbacks, and call those before doing the work of prepareForInsertion. This way it becomes impossible to make that mistake since it is the base class that makes sure that both the callbacks and the normal processing happen. You can end up with a lot of callbacks if you want this behaviour for a lot of functions. If you really do use that pattern so much, you might want to look into tools like AspectJ (or whatever the C# equivalent is) that can automate this sort of thing.

于 2009-08-13T17:40:49.720 回答
0

如果你发现你可以隐藏虚拟函数并使界面非虚拟,尝试而不是检查其他用户是否确实调用了你的函数,而是自己调用它。如果你的基本代码应该在最后被调用,它看起来像这样:

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    PrepareForInsertionImpl(pObject);
    doBasePreparing(pObject);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // nothing to do
  }

private:
  void doBasePreparing(ObjectToInsert* pObject)
  {
    // put here your code from Container::PrepareForInsertionImpl
  }
};
于 2009-08-13T19:32:37.797 回答