如何确保虚方法调用一直传播到基类?

Car*_*org 14 c++ virtual inheritance

类层次结构的一个常见错误是将基类中的方法指定为虚拟,以便继承链中的所有重写都能完成某些工作,并且忘记将调用传播到基本实现.

示例场景

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!
  }
};
Run Code Online (Sandbox Code Playgroud)

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

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

方法1

您可以使用成员变量作为标志,将其设置为虚方法的基本实现中的正确值,并在调用后检查其值.这需要使用公共非虚方法作为客户端的接口,并使虚方法受到保护(这实际上是一件好事),但它需要专门为此目的使用成员变量(需要如果虚方法必须是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);
  }
};
Run Code Online (Sandbox Code Playgroud)

方法2

另一种方法是用不透明的"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);
  }
};
Run Code Online (Sandbox Code Playgroud)

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

还有哪些其他可能性?

tpd*_*pdi 22

您已经想出了一些聪明的方法来实现这一点,(在您承认的情况下)膨胀类的成本并添加代码来解决对象的责任而不是程序员的缺陷.

真正的答案是不要在运行时这样做.这是程序员错误,而不是运行时错误.

在编译时执行:如果语言支持语言构造,则使用语言构造,或使用模式执行它(例如,模板方法),或使编译依赖于测试传递,并设置测试以强制执行它.

或者,如果传播失败导致派生类失败,则让它失败,并使用异常消息通知派生类的作者他未能正确使用基类.

  • 不能赞成这一点.如果它是虚拟的,那么你不应该计算/依赖任何基础实现. (6认同)
  • 正是我对此感到困惑.没有人应该打电话给基地会员,因为一个原因,你要覆盖它! (4认同)
  • 我可以看到想要调用基类方法,但我真的没有看到强制它的必要性.你不会每次都这样做,所以强制执行它通常不是正确的做法.如果您应该这样做,那么不这样做就是一个错误,并且可能会通过代码审查和测试找到. (2认同)

Dan*_*ing 13

您正在寻找的只是非虚拟接口模式.

它与您在此处所做的类似,但保证基类实现被调用,因为它是唯一可以调用的实现.它消除了上述示例所需的混乱.通过基类调用是自动的,因此派生版本不需要进行显式调用.

谷歌"非虚拟接口"的详细信息.

编辑:查找"模板方法模式"后,我看到它是非虚拟接口的另一个名称.我之前从未听过这个名字(我不是GoF粉丝俱乐部的卡片成员).就个人而言,我更喜欢名称非虚拟接口,因为名称本身实际上描述了模式是什么.

再次编辑:这是NVI的做法:

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.
  }
};
Run Code Online (Sandbox Code Playgroud)


Mot*_*tti 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(); 
    }
};
Run Code Online (Sandbox Code Playgroud)

如果你需要public(非virtual)函数返回一个值,那么内部函数virtual应该返回std::pair<return-type , remember_to_call_base>.


注意事项:

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


sbi*_*sbi 0

看看模板方法模式。(基本思想是您不必再调用基类方法。)