私有纯虚函数有啥意义?

Posted

技术标签:

【中文标题】私有纯虚函数有啥意义?【英文标题】:What is the point of a private pure virtual function?私有纯虚函数有什么意义? 【发布时间】:2011-04-27 14:00:31 【问题描述】:

我在一个头文件中发现了以下代码:

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 类或从它派生的类必须为那些纯虚函数提供实现。但是我不认为派生类可以访问这些私有函数来重新实现它们 - 那么为什么要让它们成为虚拟的呢?

【问题讨论】:

【参考方案1】:

主题中的问题提出了一个非常常见的混淆。这种混淆很常见,以至于C++ FAQ 长期以来一直主张反对使用私有虚拟,因为混淆似乎是一件坏事。

所以首先要摆脱混淆:是的,私有虚函数可以在派生类中被覆盖。派生类的方法不能从基类调用虚函数,但可以为它们提供自己的实现。根据 Herb Sutter 的说法,在基类中具有公共的非虚拟接口和可以在派生类中定制的私有实现,可以更好地“将接口规范与实现的可定制行为规范分离”。您可以在他的文章"Virtuality" 中阅读更多相关信息。

在我看来,您提供的代码中还有一件更有趣的事情值得更多关注。公共接口由一组重载的非虚函数组成,这些函数调用非公共的、非重载的虚函数。像往常一样,在 C++ 世界中,它是一个习语,它有一个名字,当然它也很有用。名字是(惊喜,惊喜!)

“公共重载非虚拟调用受保护非重载虚拟”

对properly manage the hiding rule 有帮助。你可以阅读更多关于它的信息here,但我会尽快解释它。

想象一下,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*/
;

【讨论】:

为什么虚函数必须是私有的?可以公开吗? 我想知道 Herb Sutter 在他的“虚拟”文章中给出的指导方针今天是否仍然有效? @Rich 你可以,但是通过将它们设为非公开,你可以更清楚地传达他们的意图。首先,如果您坚持将接口公开和实现非公开,它会显示关注点分离。其次,如果您希望继承类能够调用基实现,则可以将它们声明为受保护的;如果您只希望它们提供自己的实现而不调用基本实现,则将它们设为私有。【参考方案2】:

Private pure virtual 函数是 Non-virtual interface 成语的基础(好吧,它并不总是 pure virtual,但仍然虚拟那里)。当然,这也用于其他事情,但我发现它最有用(:一句话:在公共函数中,您可以将一些常见的东西(例如日志记录、统计信息等)放在开头,然后在函数的末尾,然后在“中间”调用这个私有虚函数,这对于特定的派生类会有所不同。像:

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

【讨论】:

【参考方案3】:

首先,这将允许派生类实现基类(包含纯虚函数声明)可以调用的函数。

【讨论】:

只有基类可以调用! @underscore_d 每个实现该函数的派生类(及其朋友)都可以调用自己的函数。 (除非明确限定,否则此类调用将被虚拟调度。) @PabloH 对!如果派生类提供了自己的覆盖,它可以调用它(无论它的覆盖是否是私有的)。如果派生类在基类中是私有的,则派生类只能调用该函数,并且派生类不会覆盖它。这似乎很明显。我不确定我的意思是什么,早在 2016 年 :-)【参考方案4】:

编辑:关于覆盖能力和访问/调用能力的澄清声明。

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

#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);

Privatevirtual 基类中的方法(如代码中的方法)通常用于实现Template Method design pattern。该设计模式允许人们在不更改基类中的代码的情况下更改基类中算法的行为。上面通过基类指针调用基类方法的代码是模板方法模式的一个简单示例。

【讨论】:

我明白了,但是如果派生类无论如何都具有某种访问权限,为什么还要将它们设为私有? @BeeBand:用户可以访问公共派生类虚拟方法覆盖,但不能访问基类。在这种情况下,派生类作者也可以保持虚拟方法覆盖私有。事实上,我将在上面的示例代码中进行更改以强调这一点。无论哪种方式,他们总是可以公开继承并覆盖私有基类虚拟方法,但他们仍然只能访问自己的派生类虚拟方法。请注意,我在覆盖和访问/调用之间进行了区分。 因为你错了。 EngineDerivedEngine 类之间的继承可见性与 DerivedEngine 可以或不能覆盖(或访问,就此而言)无关。 @wilhelmtell:叹息你当然是对的。我会相应地更新我的答案。【参考方案5】:

私有虚方法用于限制可以覆盖给定函数的派生类的数量。必须覆盖私有虚方法的派生类必须是基类的朋友。

简要说明可参见DevX.com。


EDIT 在Template Method Pattern 中有效地使用了私有虚拟方法。派生类可以覆盖私有虚拟方法,但派生类不能调用它的基类私有虚拟方法(在您的示例中,SetStateBoolSetStateInt)。只有基类才能有效调用其私有虚方法(只有派生类需要调用虚函数的基实现时,才使虚函数受保护)。

可以找到一篇关于Virtuality的有趣文章。

【讨论】:

@Gentleman...嗯,向下滚动到 Colin D Bennett 的评论。他似乎认为“私有虚函数可以被派生类覆盖,但只能从基类内部调用。”。 @Michael Goldshteyn 也这么认为。 我猜你已经忘记了基于私有的类不能被其派生类看到的原则。这就是 OOP 规则,它适用于所有 OOP 语言。为了让派生类实现其基类私有虚拟方法,它必须是基类的friend。 Qt 在实现其 XML DOM 文档模型时采用了相同的方法。 @Gentleman:不,我没有忘记。我在评论中打错了字。而不是“访问基类方法”,我应该写“可以覆盖基类方法”。派生类当然可以覆盖私有虚拟基类方法,即使它不能访问该基类方法。您指出的 DevX.com 文章不正确(公共继承)。试试我的答案中的代码。尽管私有虚拟基类方法,派生类能够覆盖它。我们不要将覆盖私有虚拟基类方法的能力与调用它的能力混淆。 @Gentleman:@wilhelmtell 在我的回答/评论中指出了错误。我关于继承影响基类方法的派生类可访问性的说法是错误的。我已删除对您的回答的冒犯性评论。 @Void,我看到派生类可以覆盖它的基类私有虚拟方法,但它不能使用它。所以,这本质上是一个模板方法模式。【参考方案6】:

TL;DR 答案:

您可以将它视为另一个级别的封装 - 介于 protectedprivate 之间:您不能从子类调用它,但可以覆盖它。

在实现Template Method 设计模式时很有用。您可以使用 protected,但 privatevirtual 一起可能被认为是更好的选择,因为封装更好。

【讨论】:

以上是关于私有纯虚函数有啥意义?的主要内容,如果未能解决你的问题,请参考以下文章

多态—— 纯虚函数和抽象类

抽象类,虚函数,纯虚函数的意义

虚函数纯虚函数和接口的实用方法和意义

54)纯虚函数和抽象类

c++11纯虚类之友无法访问私有方法

关于虚函数与纯虚函数的区别