在C++中用私有函数覆盖公共虚函数

67

有没有任何理由使得重写的C++虚函数的权限与基类不同?这样做是否存在任何危险?

例如:

class base {
    public:
        virtual int foo(double) = 0;
}

class child : public base {
    private:
        virtual int foo(double);
}

C++ faq表示这是个坏主意,但没有说明原因。

我在一些代码中看到过这种用法,我认为作者试图使类成为最终类(final),基于这样的一个假设:不可能重载(private)成员函数。然而,这篇文章展示了一个重载(private)函数的例子。当然,C++ FAQ中的另一部分建议避免这样做。

我的具体问题是:

  1. 使用派生类和基类中不同的权限来定义虚方法是否存在技术问题?

  2. 这么做是否存在合理的原因?


3
我们是在重新发明“保护”的概念吗? - Filip Ekberg
7个回答

49

你会惊讶地发现,如果你有一个子类,你不能调用foo方法,但是你可以将其转换为基类,然后调用foo方法。

child *c = new child();
c->foo; // compile error (can't access private member)
static_cast<base *>(c)->foo(); // this is fine, but still calls the implementation in child

我认为你可能可以构造一个例子,在这个例子中你不想要一个函数被暴露,除非你将其视为基类的一个实例。但是,这种情况出现的事实表明,在某个地方存在着糟糕的OO设计,应该进行重构。


1
Qt做到了这一点,把我带到了这里 :) 谢谢。 - mlvljr
26
@mlvljr:作为一件天文事件,我非常棒! 翻译:但作为一件天文事件,我还是相当棒的! - Eclipse

40
问题在于基类方法是声明其接口的方式。本质上,它在说:“这些是您可以对此类对象执行的操作。”
当在派生类中将基类声明为公共私有时,您正在拿走某些东西。现在,即使派生对象“是一个”基类对象,您也无法对派生类对象执行应该能够对基类对象执行的操作,从而违反里氏替换原则
这会在程序中引起“技术”问题吗?也许不会。但是这可能意味着您的类的对象不会像用户所期望的那样表现。
如果您发现自己处于这种情况(除了另一个答案中提到的弃用方法的情况之外),那么很可能您具有继承模型,其中继承并不真正地模拟“是-一个”(例如Scott Myers的示例Square继承自Rectangle,但您无法像对矩形一样单独更改正方形的宽度)。您可能需要重新考虑您的类关系。

8
没有技术问题,但最终会出现这样一种情况:公开可用的函数将取决于您是否具有基础或派生指针。
在我看来,这将是奇怪和混乱的。

4

如果您正在使用私有继承,这将非常有用——也就是说,您想要重用基类的(定制的)功能,但不想重用其接口。


3
可以这样做,但很少会带来好处。例如,在我们的代码库中,我们使用了一个包含公共函数的库,但现在不鼓励使用该函数,因为存在其他潜在问题(有更安全的方法调用)。我们还恰好有一个从该类派生的类,许多代码直接使用它。因此,我们在派生类中将给定函数设为私有,以帮助大家记住尽量不要使用它。这并没有消除使用它的能力,但当代码尝试编译时,它会捕获一些用法,而不是在代码审查后期才发现。

1
  1. 如果你所说的技术问题是指存在隐藏的运行时成本,那么这里并不存在技术问题。
  2. 如果你是公开继承基类,那么不应该这样做。如果你是通过保护或私有方式继承,那么这可以帮助防止使用那些没有基类指针就没有意义的方法。

1
一个很好的私有继承使用案例是Listener/Observer事件接口。
私有对象的示例代码:
class AnimatableListener {
  public:
    virtual void Animate(float delta_time);
};

class BouncyBall : public AnimatableListener {
  public:
    void TossUp() {}
  private:
    void Animate(float delta_time) override { }
};

一些对象的用户需要父级功能,而另一些则需要子级功能:

class AnimationList {
   public:
     void AnimateAll() {
       for (auto & animatable : animatables) {
         // Uses the parent functionality.
         animatable->Animate();
       }
     }
   private:
     vector<AnimatableListener*> animatables;
};

class Seal {
  public:
    void Dance() {
      // Uses unique functionality.
      ball->TossUp();
    }
  private:
    BouncyBall* ball;
};

这样AnimationList就可以持有对父级的引用并使用父级功能。而Seal则持有对子级的引用并使用独特的子级功能,忽略了父级的功能。在这个例子中,Seal不应该调用Animate。如上所述,可以通过将对象转换为基本对象来调用Animate,但这更加困难,通常不应该这样做。

监听器/观察者接口,接收回调。 - dashesy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接