58

是否有任何理由使重写的 C++ 虚函数的权限不同于基类?这样做有什么危险吗?

例如:

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

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

C++ 常见问题解答说这是一个坏主意,但没有说明原因。

我在一些代码中看到了这个习语,我相信作者试图使这个类成为最终的,基于不可能覆盖私有成员函数的假设。然而,这篇文章展示了一个重写私有函数的例子。当然,C++ 常见问题的另一部分建议不要这样做。

我的具体问题:

  1. 在派生类和基类中对虚拟方法使用不同的权限是否有任何技术问题?

  2. 有任何正当理由这样做吗?

4

7 回答 7

42

你确实得到了令人惊讶的结果,如果你有一个孩子,你不能调用 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 设计。

于 2009-01-27T18:27:21.647 回答
34

问题是基类方法是它声明其接口的方式。它本质上是在说,“这些是你可以对这个类的对象做的事情。”

在派生类中,您将 Base 声明为公有私有的某些内容设置为带走某些内容。现在,即使 Derived 对象“是”Base 对象,您应该能够对 Base 类对象执行的操作却不能对 Derived 类对象执行,从而打破了Liskov 替换原则

这会在您的程序中导致“技术”问题吗?也许不吧。但这可能意味着您的类的对象不会以您的用户期望的方式运行。

如果您发现自己处于您想要的情况(除了另一个答案中提到的已弃用方法的情况),那么您很可能有一个继承模型,其中继承并不是真正建模“is-a”(例如,Scott Myers 的示例 Square 继承自 Rectangle,但您不能像矩形那样独立于其高度来更改 Square 的宽度)并且您可能需要重新考虑您的类关系。

于 2009-01-27T19:00:40.363 回答
6

没有技术问题,但您最终会遇到这样一种情况,即公开可用的函数将取决于您是否具有基指针或派生指针。

在我看来,这将是奇怪和令人困惑的。

于 2009-01-27T18:26:46.017 回答
4

如果您使用私有继承,它会非常有用——即您想重用基类的(定制)功能,而不是接口。

于 2009-01-27T19:17:44.090 回答
3

这是可以做到的,而且非常偶尔会带来好处。例如,在我们的代码库中,我们正在使用一个库,其中包含一个具有我们过去使用的公共函数的类,但现在由于其他潜在问题不鼓励使用(有更安全的方法可以调用)。我们也碰巧有一个派生自该类的类,我们的很多代码都直接使用它。因此,我们在派生类中将给定函数设为私有,以帮助每个人记住,如果他们能提供帮助,就不要使用它。它不会消除使用它的能力,但它会在代码尝试编译时捕获一些用途,而不是稍后在代码审查中。

于 2009-01-27T18:41:57.797 回答
1
  1. 没有技术问题,如果您指的是技术,因为存在隐藏的运行时成本。
  2. 如果你公开继承 base,你不应该这样做。如果您通过受保护或私有继承,那么这有助于防止使用没有意义的方法,除非您有一个基指针。
于 2009-01-27T18:42:02.150 回答
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可以通过转换为基础对象来调用,但这更困难,通常不应该这样做。

于 2017-01-05T19:26:40.993 回答