C ++中的私有虚拟方法


125

在C ++中将私有方法虚拟化的好处是什么?

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

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

9
我认为问题是倒退的。将某些东西虚拟化的原因始终是相同的:允许派生类覆盖它。所以问题应该是:将虚拟方法设为私有的优点是什么?答案是:默认情况下将所有内容设为私有。:-)
ShreevatsaR 2014年

1
@ShreevatsaR但是您甚至都没有回答自己的问题……
Spencer

@ShreevatsaR我以为您是用不同的方式向后指的:将虚拟方法设为私有的优点是什么?
彼得-恢复莫妮卡

Answers:


115

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

准则2:建议将虚拟函数设为私有。

这样,派生类就可以根据需要重写函数以自定义行为,而无需进一步通过使派生类调用虚拟函数来直接公开虚拟函数(如果仅对函数进行保护的话,则是可能的)。关键是存在虚拟功能以允许自定义。除非也需要直接在派生类的代码中调用它们,否则就不必将它们设为私有


您可能会从我的答案中猜到,我认为Sutter的准则3会将准则2推到了窗外。
斯宾塞

66

如果该方法是虚拟的,则即使它是私有的,也可以被派生类覆盖。调用虚拟方法时,将调用覆盖的版本。

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


41
看来,C ++ FAQ Lite从此改变了它的建议:“ C ++ FAQ以前建议使用受保护的虚拟机,而不是私有虚拟机。但是,私有虚拟机方法现在已经很普遍了,因此新手的困惑不再是一个问题。
Zack The人类

19
但是,专家的困惑仍然令人担忧。坐在我旁边的四位C ++专业人员都不知道私有虚拟机。
Newtonx '17

12

尽管所有调用都将虚拟成员声明为私有,但该论点根本无法成立。通常,派生类对虚拟函数的替代将必须调用基类版本。如果声明了它就不能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 ++委员会放宽“私有”以允许这种特定的访问,那么我将全部用于私有虚拟功能。就目前情况而言,仍然建议我们在马被盗后锁上谷仓门。


3
我发现您的论点无效。作为API的开发人员,您应该努力争取一个难以正确使用的接口,并且不要因自己的错误而导致其他开发人员的忙。您可以在示例中执行的操作仅可以使用私有虚拟方法来实现。
sigy

1
我不是那个意思 但是您可以重组代码以实现相同的效果,而无需调用私有的基类函数
sigy

3
在您的示例中,您想扩展的行为set_data。指令m_data = ndata;cleanup();因此可以被认为对于所有实现都必须保持不变。因此,请使其成为cleanup()非虚拟的和私有的。将调用添加到另一个虚拟的私有方法以及您的类的扩展点。现在,您的派生类不再需要调用base的类cleanup(),您的代码保持整洁,并且您的接口很难被错误地使用。
sigy

2
@sigy只是移动球门柱。您需要超越典型的例子。当还有其他后代需要调用cleanup()链中的所有s时,参数将分散。还是为链中的每个后代推荐一个额外的虚函数?ck 甚至Herb Sutter都在其准则3中允许受保护的虚拟功能作为漏洞。无论如何,没有一些实际的代码,您将永远无法说服我。
斯宾塞

2
然后让我们同意不同意;)
sigy

9

在阅读Scott Meyers的“ Effective C ++”,第35项:考虑虚拟函数的替代方法时,我首先遇到了这个概念我想参考Scott Mayers可能感兴趣的其他内容。

通过非虚拟接口惯用法,它是模板方法模式的一部分:面向公众的方法不是虚拟的;相反,它们包装了私有的虚拟方法调用。然后,基类可以在私有虚拟函数调用之前和之后运行逻辑:

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

我认为这是一个非常有趣的设计模式,并且我确定您可以看到添加的控件如何有用。

  • 为什么要使虚函数private?最好的原因是我们已经提供了一种public面对方法。
  • 为什么不简单地做到这protected一点,以便我可以将该方法用于其他有趣的事情?我想它总是取决于您的设计以及您对基类的适应程度。我认为派生类的制造者应专注于实现所需的逻辑;其他一切都已经照顾好了。另外,还有封装问题。

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


3

我使用它们来允许派生类为基础类“填充空白”,而不会给最终用户带来这样的漏洞。例如,我有一个来自一个通用基础的高度状态对象,该基础只能实现整个状态机的2/3(派生类根据模板参数提供剩余的1/3,而基础不能是用于其他原因)。

我需要有一个通用的基类,以使许多公共API正常工作(我正在使用可变参数模板),但是我不能让那个对象泛滥成灾。更糟糕的是,如果我将弹坑以纯虚拟功能的形式留在状态机中,但不在“私有”中的任何地方,我将允许从其子类之一派生的聪明或无知的用户重写用户永远不应接触的方法。因此,我将状态机“大脑”置于PRIVATE虚拟函数中。然后,基类的直接子代将其非虚拟替代项填充为空白,用户可以安全地使用结果对象或创建自己的进一步派生类,而不必担心搞乱状态机。

至于说您不应该拥有公共虚拟方法的说法,我说BS。用户可以像公共虚拟一样轻易地不正确地覆盖私有虚拟,因为它们毕竟是在定义新的类。如果公众不应该修改给定的API,请不要将其虚拟化为可公开访问的对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.