C++中的私有虚函数

159

C++中将私有方法声明为虚函数的优势是什么?

我在一个开源的C++项目中注意到了这一点:

class HTMLDocument : public Document, public CachedResourceClient {
private:
    virtual bool childAllowed(Node*);
    virtual PassRefPtr<Element> createElement(const AtomicString& tagName, ExceptionCode&);
};

15
我认为这个问题问反了。将某些东西变成虚拟的原因总是相同的:让派生类重写它。所以问题应该是:将虚拟方法设为私有的有什么优势?答案是:默认情况下使所有内容都变为私有。 :-) - ShreevatsaR
3
但是你甚至没有回答自己的问题...... - Spencer
@ShreevatsaR 我以为你指的是另一种方式的反向:使虚方法私有的优点是什么? - Peter - Reinstate Monica
6个回答

154

Herb Sutter在这里非常好地解释了它。

指南2:尽量将虚函数设为私有。

这样,派生类就可以重写函数以根据需要自定义行为,而不必通过将它们设置为可由派生类调用(如果函数只是受保护,则可能会发生这种情况)而进一步直接公开虚函数。关键是虚函数存在于允许自定义的目的;除非它们还需要从派生类代码内部直接调用,否则永远没有必要将它们设置为除了私有以外的其他权限。


3
如果一个派生类需要覆盖方法但需要从内部调用父类方法怎么办?这很常见,如果私有虚函数阻止了这种情况,我无法想象它会被推荐。C++ 是否有像 super(...) 这样的机制可以在重写版本中调用父类方法,即使它是私有的也能正常工作? - flarn2006
1
@flarn2006 指南第三条:只有在派生类需要调用虚函数的基本实现时,才将虚函数设置为受保护的。 - luizfls
但是你如何调用基础函数? - user13947194
1
@user13947194 基类名称::方法名称(任何, 参数, 你, 需要); - Marc
@Mar 但是你的方法是私有的。 - user13947194
显示剩余2条评论

79

如果方法是虚函数,即使它是私有的,派生类也可以重写它。当调用虚方法时,将调用已重写的版本。

(与Herb Sutter在Prasoon Saurav的答案中引用的相反,C ++ FAQ Lite不建议使用私有虚拟,主要是因为它经常会让人们感到困惑。)


47
看起来 C++ FAQ Lite 已经改变了它的建议:"C++ FAQ 以前推荐使用 protected virtuals 而不是 private virtuals。但是,使用 private virtual 方法已经足够普遍,以至于新手的困惑不再是一个问题。" - Zack The Human
39
然而,专家们的困惑仍然是一个问题。坐在我旁边的四名C++专业人员中没有人意识到私有虚拟函数。 - Newtonx

14

尽管有很多人呼吁将虚拟成员声明为私有的,但这个论点并不站得住脚。通常,派生类重写虚函数时需要调用基类版本,如果将其声明为private,就无法实现:

class Base
{
 private:

 int m_data;

 virtual void cleanup() { /*do something*/ }

 protected:
 Base(int idata): m_data (idata) {}

 public:

 int data() const { return m_data; }
 void set_data (int ndata) { m_data = ndata; cleanup(); }
};

class Derived: public Base
{
 private:
 void cleanup() override
 {
  // do other stuff
  Base::cleanup(); // nope, can't do it
 }
 public:
 Derived (int idata): base(idata) {}
};

您需要将基类方法声明为protected

然后,您需要通过注释指示应该重写但不调用该方法,这是一种不太理想的丑陋方法。

class Base
{
 ...
 protected:
 // chained virtual function!
 // call in your derived version but nowhere else.
 // Use set_data instead
 virtual void cleanup() { /* do something */ }
 ...

因此,Herb Sutter的指导方针#3...但无论如何,马已经出了谷仓。
当您声明某些内容为protected时,您隐含地信任任何派生类的编写者理解和正确使用受保护的内部,就像friend声明暗示对private成员有更深层次的信任一样。
违反这种信任而获得不良行为(例如由于不愿阅读您的文档而被标记为“无知”)的用户只能责怪自己。
更新:我收到一些反馈称,您可以使用私有虚拟函数以这种方式“链接”虚拟函数实现。如果是这样,我肯定想看看。
我使用的C ++编译器绝对不会让派生类实现调用私有基类实现。
如果C ++委员会放宽"private"以允许这种特定访问,那么我完全支持私有虚拟函数。就目前而言,我们仍然被建议在马被偷后锁上谷仓门。

5
我认为你的论点无效。作为 API 的开发者,你应该努力创造一个使用起来 不容易出错 的接口,而不是让其他开发者因此陷入你自己的错误中。对于你在例子中想要实现的功能,只需要用私有虚拟方法即可实现。 - sigy
3
这不是我想说的。但是你可以重构你的代码,达到相同的效果,而无需调用一个私有基类函数。 - sigy
6
在你的例子中,你想扩展 set_data 的行为。因此,指令 m_data = ndata;cleanup(); 可以被视为必须对所有实现都适用的不变量。因此,将 cleanup() 设为非虚函数且私有化。添加对另一个私有虚拟方法的调用,该方法是类的扩展点。现在,派生类不再需要调用基类的 cleanup() 方法,代码保持干净,接口难以被错误使用。 - sigy
4
@sigy 这只是移动了目标。 你需要超越最小的例子。 当有进一步的后代需要调用链中的所有cleanup()时,这个论点就会崩溃。 或者你建议在链中的每个后代中添加额外的虚拟函数? 呃。即使Herb Sutter允许受保护的虚拟函数作为他的第3个指南中的漏洞。 无论如何,没有一些实际的代码,你永远不会说服我。 - Spencer
3
你的两个论点都未通过我的检查。清理行为应从类中提取出来,使具有多态清理策略的具体类。组合应优先于继承。即使我们超越最小示例,对于任何其他行为也适用相同的规则。一个类应该尽力只拥有一个职责。准则-只覆盖纯虚函数。 - Dragan
显示剩余6条评论

12

在阅读Scott Meyers的《Effective C++》中,我第一次接触到这个概念,即“Item 35: 考虑替代虚函数”。我想提及Scott Meyers以供其他可能感兴趣的人参考。

这是通过非虚拟接口习语实现的“模板方法模式”,公共方法不是虚拟的,而是包装私有的虚拟方法调用。基类可以在私有虚拟函数调用前后运行逻辑:

public:
  void NonVirtualCalc(...)
  {
    // Setup
    PrivateVirtualCalcCall(...);
    // Clean up
  }

我认为这是一种非常有趣的设计模式,而且我相信你可以看出添加控制的用处。

  • 为什么要将虚函数设为private?最好的原因是我们已经提供了一个public接口。
  • 为什么不直接将其设为protected,以便我可以将该方法用于其他有趣的事情?我想这将始终取决于您的设计以及您如何认为基类适合。我会认为派生类制作者应该专注于实现所需逻辑;其他所有内容都已经处理好了。此外,还涉及到封装的问题。

从C++的角度来看,即使您无法从类中调用它,覆盖私有虚方法也是完全合法的。这支持上述描述的设计。


5
我将它们用于允许派生类为基类“填补空白”,而不向最终用户展示这样的漏洞。例如,我有高度状态化的对象从一个共同的基类派生,它们只能实现总状态机的2/3(派生类根据模板参数提供其余的1/3,基类由于其他原因不能成为模板)。
为了使许多公共API起作用,我需要有一个共同的基类,但我不能让那个对象出现在外面。更糟糕的是,如果我在状态机中留下弹坑——以纯虚函数的形式——除了“Private”之外的任何地方,我都会让一个聪明或愚蠢的用户从一个子类中派生并重写用户永远不应该接触的方法。因此,我将状态机“核心”放在“PRIVATE”虚函数中。然后,基类的直接子类在它们的非虚拟覆盖中填补空白,用户可以安全地使用生成的对象或创建自己的进一步派生类,而不必担心破坏状态机。
至于你不应该拥有公共虚函数的论点,我认为这是胡说八道。用户可以像公共虚拟一样不正确地覆盖私有虚拟,毕竟他们正在定义新类。如果公共用户不应该修改给定API,请根本不要在公共可访问对象中将其设置为虚拟。

1
如果有帮助的话,C++11有一个“final”关键字来防止进一步的覆盖。 - Maitre Bart

1

另一个原因可能是所有继承类的共同逻辑:

class Base
{
public:
    void interfaceMethod() {
        /** common logic **/
        factoryMethod();
    }
private:
    virtual void factoryMethod() = 0;
};

class concrete1 : Base 
{
private:
    void factoryMethod() override {
        /** specific logic **/
    }
};
    
class concrete2 : Base 
{
private:
    void factoryMethod() override {
        /** specific logic **/
    }
};

然后,为了

 Base* obj = new concrete1();

或者

 Base* obj = new concrete2();

那么obj.interfaceMethod()将执行每个具体对象的通用逻辑和特定逻辑。


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