一个私有的纯虚函数的作用是什么?

152
我在一个头文件中看到了以下代码:
class Engine
{
public:
    void SetState( int var, bool val );
    {   SetStateBool( int var, bool val ); }

    void SetState( int var, int val );
    {   SetStateInt( int var, int val ); }
private:
    virtual void SetStateBool(int var, bool val ) = 0;    
    virtual void SetStateInt(int var, int val ) = 0;    
};

对我来说,这意味着Engine类或其派生类必须为这些纯虚函数提供实现。但是我认为派生类无法访问这些私有函数以重新实现它们-那么为什么要将它们定义为虚函数呢?

6个回答

221
这个话题中的问题表明了一个相当普遍的困惑。这种困惑是足够常见的,以至于 C++ FAQ 长期以来都反对使用私有虚函数,因为困惑似乎是一件坏事。
首先要消除这种困惑:是的,在派生类中可以重写私有虚函数。派生类的方法不能调用基类中的虚函数,但是它们可以为其提供自己的实现。根据Herb Sutter的说法,在基类中具有公共非虚接口和可以在派生类中自定义的私有实现,可以更好地"将接口规范与可定制行为的实现规范分离"。你可以在他的文章《Virtuality》中了解更多信息。
然而,在你提供的代码中还有一件更有趣的事情,我认为它值得更多的关注。公共接口由一组重载的非虚函数组成,这些函数调用非公开、非重载的虚函数。通常在 C++ 的世界中,这是一种惯用语法,它有一个名字,当然也很有用。这个名字是(惊喜!)
"Public Overloaded Non-Virtuals Call Protected Non-Overloaded Virtuals"
它有助于 正确管理隐藏规则。你可以在这里阅读更多内容,但我会简要解释一下。
想象一下,Engine 类的虚函数也是其接口,它是一组不是纯虚的重载函数。如果它们是纯虚的,那么仍然可能遇到下面描述的同样问题,只不过在类层次结构中更低的位置。
class Engine
{
public:
    virtual void SetState( int var, bool val ) {/*some implementation*/}
    virtual void SetState( int var, int val )  {/*some implementation*/}
};

现在假设您想创建一个派生类,并且您需要仅为接受两个整数参数的方法提供新实现。

class MyTurbochargedV8 : public Engine
{
public:
    // To prevent SetState( int var, bool val ) from the base class,
    // from being hidden by the new implementation of the other overload (below),
    // you have to put using declaration in the derived class
    using Engine::SetState;

    void SetState( int var, int val )  {/*new implementation*/}
};

如果你忘记在派生类中加入using声明(或重新定义第二个重载函数),那么在下面的情况下可能会出现问题。

MyTurbochargedV8* myV8 = new MyTurbochargedV8();
myV8->SetState(5, true);

如果您没有防止隐藏 Engine 成员,那么该语句会执行:

myV8->SetState(5, true);

从派生类调用void SetState( int var, int val ),将true转换为int

如果接口不是虚拟的且虚拟实现是非公共的(就像您的示例一样),那么派生类的作者可以更简单地编写代码,不必考虑一个问题:

class MyTurbochargedV8 : public Engine
{
private:
    void SetStateInt(int var, int val )  {/*new implementation*/}
};

为什么虚函数必须是私有的?它可以是公共的吗? - Rich
我想知道Herb Sutter在他的“Virtuality”文章中提出的指导方针今天是否仍然适用? - nurabha
2
@Rich 你可以这样做,但是将它们设为非公共的,可以更清晰地传达它们的意图。首先,如果你坚持将接口设为公共的,而将实现设为非公共的,它显示了关注点的分离。其次,如果你希望继承类能够调用基本实现,你可以将它们声明为受保护的;如果你只希望它们提供自己的实现而不调用基本实现,你可以将它们设为私有的。 - Dan

44

私有纯虚函数非虚拟接口习惯用法的基础(好吧,它不总是绝对的虚函数,但在这里仍然是虚函数)。当然,它也用于其他事情,但我发现这对我最有用(: 简单来说,您可以在公共函数中放置一些常见的东西(例如记录日志、统计等),在函数的开头和结尾,并在“中间”调用此私有虚函数,这将针对具体派生类而异。像这样的:

class Base
{
    // ..
public:
    void f();
private:
    virtual void DerivedClassSpecific() = 0;
   // ..
};
void Base::f()
{
    //.. Do some common stuff
    DerivedClassSpecific();
    //.. Some other common stuff
}
// ..

class Derived: public Base
{
    // ..
private:
    virtual void DerivedClassSpecific();
    //..
};
void Derived::DerivedClassSpecific()
{
    // ..
}

纯虚函数 - 只是要求派生类实现它。

编辑: 更多相关信息: Wikipedia::NVI-idiom


17

首先,这样可以让派生类实现一个基类中声明的纯虚函数并且可以被基类调用。


6
只有基类才能调用! - underscore_d
1
@underscore_d 每个派生类实现该函数(及其友元函数)时都可以调用自己的函数。(除非明确限定,否则此调用将虚拟分派。) - Pablo H
@PabloH 对的!如果派生类提供了自己的覆盖,它可以调用它(无论它的覆盖是否为私有)。只有在基类中它是私有的,并且派生类没有覆盖它时,派生类才不能调用该函数。这似乎很明显。我不确定我2016年时的观点是什么 :-) - underscore_d

4

编辑:澄清了有关覆盖和访问/调用能力的陈述。

它将能够覆盖那些私有函数。例如,以下虚构的示例起作用(编辑:将派生类方法设置为私有,并将派生类方法调用从 main() 中删除,以更好地展示使用的设计模式意图。):

#include <iostream>

class Engine
{
public:
  void SetState( int var, bool val )
  {
    SetStateBool( var, val );
  }

  void SetState( int var, int val )
  {
    SetStateInt( var, val );
  }

private:

    virtual void SetStateBool(int var, bool val ) = 0;
    virtual void SetStateInt(int var, int val ) = 0;

};

class DerivedEngine : public Engine
{
private:
  virtual void SetStateBool(int var, bool val )
  {
    std::cout << "DerivedEngine::SetStateBool() called" << std::endl;
  }

  virtual void SetStateInt(int var, int val )
  {
    std::cout << "DerivedEngine::SetStateInt() called" << std::endl;
  }
};


int main()
{
  DerivedEngine e;
  Engine * be = &e;

  be->SetState(4, true);
  be->SetState(2, 1000);
}

Private virtual方法是在基类中使用的,就像您代码中的那些方法一样,通常用于实现模板方法设计模式。该设计模式允许在不更改基类代码的情况下更改算法的行为。上面的代码中,通过基类指针调用基类方法是模板方法模式的一个简单示例。


我明白了,但如果派生类已经有某种访问方式,为什么还要将它们设置为私有的呢? - BeeBand
@BeeBand:用户将可以访问公共派生类虚方法覆盖,但无法访问基类的虚方法。在这种情况下,派生类作者可以将虚方法覆盖保持为私有。实际上,我将更改上面的示例代码以强调这一点。无论哪种方式,他们始终可以公开继承并覆盖私有基类虚拟方法,但仍然只能访问自己的派生类虚拟方法。请注意,我正在区分覆盖和访问/调用之间的区别。 - Void
因为你是错的。Engine类和DerivedEngine类之间的继承可见性与DerivedEngine能否覆盖(或访问)无关。 - wilhelmtell
@wilhelmtell:叹气 当然,你是正确的。我会相应地更新我的答案。 - Void

2

私有虚方法用于限制可以覆盖给定函数的派生类的数量。必须覆盖私有虚方法的派生类将需要成为基类的友元。

可以在DevX.com上找到简要说明。


编辑 私有虚方法在模板方法模式中被有效使用。派生类可以覆盖私有虚方法,但是派生类不能调用其基类的私有虚方法(在您的示例中为SetStateBoolSetStateInt)。只有基类可以有效地调用其私有虚方法(仅当派生类需要调用虚函数的基本实现时,才将虚函数设置为受保护的)。

关于虚拟性的一篇有趣的文章。


2
先生们...嗯,往下滚动到Colin D Bennett的评论。他似乎认为"私有的虚函数可以被派生类重写,但只能在基类内部调用"。@Michael Goldshteyn也持有同样的观点。 - BeeBand
我猜你忘记了一个原则,那就是基于私有的类不能被其派生类看到。这是面向对象编程的规则,并适用于所有支持面向对象编程的语言。为了让派生类实现其基类的私有虚拟方法,它必须是基类的“友元”。Qt在实现其XML DOM文档模型时采取了相同的方法。 - Buhake Sindi
@绅士们:不,我没有忘记。我在评论中打错了一个字。我应该写“可以重写基类方法”,而不是“可以访问基类方法”。即使派生类无法访问基类方法,它仍然可以重写私有虚拟基类方法。你指出的DevX.com文章是错误的(公共继承)。请尝试我的答案中的代码。尽管存在私有虚拟基类方法,派生类仍能够重写它。让我们不要混淆重写私有虚拟基类方法的能力和调用它的能力。 - Void
@Gentleman:@wilhelmtell指出了我的回答/评论中的错误。我关于继承影响派生类访问基类方法的说法是错误的。我已经将冒犯性评论从你的回答中删除了。 - Void
@Void,我看到一个派生类可以覆盖其基类的私有虚方法,但它不能使用它。因此,这本质上是一个模板方法模式。 - Buhake Sindi

1

简短回答:

你可以将其视为另一层封装 - 介于protectedprivate之间:你不能从子类中调用它,但你可以覆盖它。

在实现模板方法设计模式时很有用。你可以使用protected,但privatevirtual结合可能被认为是更好的选择,因为它具有更好的封装性。


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