代码之家  ›  专栏  ›  技术社区  ›  Carl Seleborg

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

  •  14
  • Carl Seleborg  · 技术社区  · 15 年前

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

    示例场景

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

    我的问题是: 有没有好的方法/模式来确保基本实现总是在调用链的末尾被调用?

    我知道有两种方法。

    方法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);
      }
    };
    

    方法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);
      }
    };
    

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

    还有其他的可能性吗?

    7 回复  |  直到 15 年前
        1
  •  22
  •   tpdi    15 年前

    您已经想出了一些聪明的方法来实现这一点,代价是(正如您所承认的那样)使类膨胀,并添加代码来解决对象的责任,而不是程序员的不足。

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

    在编译时执行:如果语言支持,则使用语言构造,或者使用强制执行的模式( 例如, ,或者使编译依赖于通过的测试,并设置测试以强制执行。

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

        2
  •  13
  •   Dan Moulding    15 年前

    您要寻找的只是非虚拟接口模式。

    它与您在这里所做的类似,但是基类实现是 放心 因为它是唯一 可以 被召唤。它消除了上述示例所需的混乱。并且通过基类的调用是自动的,所以派生版本不需要进行显式调用。

    谷歌“非虚拟界面”了解详情。

    编辑 :在查找“template method pattern”之后,我发现它是非虚拟接口的另一个名称。我以前从来没听说过这个名字(我并不是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.
      }
    };
    
        3
  •  6
  •   Motti    15 年前

    当只有一个继承级别时,可以使用 template method pattern 其中公共接口是非虚拟的并调用虚拟实现函数。然后基地的逻辑进入公共功能,这是肯定会被调用的。

    如果您有不止一个继承级别,并且希望每个类都调用它的基类,那么您仍然可以使用模板方法模式,但有一个扭曲, 使虚拟函数的返回值只能由 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 (非 virtual )函数返回一个值 事实上的 一个人应该回来 std::pair< 返回类型 , remember_to_call_base> .


    注意事项:

    1. remember_to_call_base 有一个显式构造函数声明为private,因此只有 friend (在这种情况下 基础 可以创建此类的新实例。
    2. 记得打电话给基地 有一个显式定义的复制构造函数,所以 the compiler will create one 具有 公众的 可访问性,允许从 基础 实施。
    3. 记得打电话给基地 protected 截面 基础 ,如果是在 private 部分 衍生的 根本无法引用它。
        4
  •  4
  •   quamrana Ryuzaki L    15 年前

    一种完全不同的方法是注册函子。派生类将在派生类构造函数中向基类注册某些函数(或成员函数)。当客户机调用实际函数时,它是基类函数,然后遍历已注册的函数。这可以扩展到许多级别的继承,每个派生类只需要关心自己的函数。

        5
  •  0
  •   sbi    15 年前

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

        6
  •  0
  •   redtuna    15 年前

    一种解决方法是完全不使用虚拟方法,而是允许用户注册回调,并在执行prepareForInsertion之前调用这些方法。这样就不可能犯这个错误,因为它是确保回调和正常处理都发生的基类。如果你想要这个行为有很多功能,你可以得到很多回调。如果你真的使用了这么多的模式,你可能想看看像AspectJ之类的工具(或者不管C等价物是什么),这样可以自动实现这类事情。

        7
  •  0
  •   Tadeusz Kopec for Ukraine yespbs    15 年前

    如果您发现可以隐藏虚拟函数并使接口非虚拟,尝试检查其他用户是否调用了您的函数,只需自己调用即可。如果您的基本代码应该在最后调用,它将如下所示:

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