在被标记为final的类的构造函数中调用虚函数是不好的实践吗?

19

通常从构造函数中调用虚函数被认为是一种不好的做法,因为子对象中重写的函数在对象尚未构建完成时不会被调用。

但是,请考虑以下类:

class base
{
public:
   base() {}
   ~base() {}

private:
   virtual void startFSM() = 0;
};

class derived final : public base
                    , public fsm_action_interface
{
public:
    derived() : base{}
              , theFSM_{}
    {  startFSM(); }


    /// FSM interface actions

private:
   virtual void startFSM()
   { theFSM_.start(); }

private:
    SomeFSMType theFSM_;
}
在这种情况下,类derived被标记为final,因此不会存在其他子对象。因此,虚函数调用将正确解析(到最派生的类型)。
这仍然被认为是不良做法吗?

@mark是一个非常有信息量的问题,我不知道从派生consteuctor调用虚函数在覆盖行为中有不同的行为。这种行为是否符合ISO标准,并且实际上会因编译器而异?还有一点需要注意的是,虚方法被设置为私有的,而您是公开继承。 - The Floating Brain
3
如果一年后你决定derived不应该是final了,会发生什么? - T.C.
2
如果你的同学或同事认为从构造函数中调用虚函数是不好的做法,因为子对象中重写的函数还没有被调用,那么他们需要接受教育。据我所知,在C++程序员中这不是一个普遍的观点。但如果真是这样,那么整个C++程序员群体都需要接受教育。 - Cheers and hth. - Alf
base中的哪个方法通常调用startFSM?这似乎很重要,这样我们才能做出完全明智的决定。 - Mark B
@Jeffrey:我认为他在谈论派生类,以及相应的子对象,这些子对象属于最终派生对象。从我的理解来看,他的意思是,在C++中,这些调用被认为是不好的实践,因为它们不像Java或C#中那样不安全。 - Cheers and hth. - Alf
显示剩余2条评论
3个回答

9
这种做法通常被认为是不好的实践,因为这种情况几乎总是表示设计有问题。你必须在代码中注释说明为什么在这种情况下可以工作。
T.C.上面的评论强调了这被认为是不好的实践的原因之一。
如果一年后你决定derived并不应该是final会发生什么?
话虽如此,在上面的例子中,该模式将无问题地工作。这是因为最派生类型的构造函数是调用虚函数的函数。当基类的构造函数调用解析为子类型实现的虚函数时,就会出现这个问题。在C++中,这样的函数不会被调用,因为在基类构造期间,这样的调用永远不会到达比当前执行的构造函数或析构函数更派生的类。本质上,您会得到您没有预期的行为。
编辑:所有(正确/非错误)的C++实现都必须调用当前构造函数层次结构中定义的函数版本,而不再进一步。

C++ FAQ Lite在第23.7节对此进行了相当详细的说明。

Scott Meyers在Effective C++的第9条中也谈到了从构造函数和析构函数中调用虚函数的一般问题。


1
“这仍然被认为是不良实践,因为这种情况几乎总是表示设计有问题”,但是首先调用本身并不是不良实践(但在最终类中声明成员函数为虚函数是没有意义的,因此确实是不良实践和不良设计)。 - Cheers and hth. - Alf
1
@Alf:最终的派生类继承了该成员函数上的虚拟修饰符...声明成员函数为虚拟可能是没有意义的,因为无论基类是否声明它为虚拟,它都将是虚拟的,但记录它是虚拟的非常普遍。当然,使用新的“override”关键字进行记录会更好。 - Ben Voigt
1
@BenVoigt:谢谢,我没注意到。我会使用override而不是virtual来说明这是一个实现。期望好代码的阅读体验... - Cheers and hth. - Alf
@bstar55:你对“every”的结论并不正确。读者不需要分享作者的观点。如果你所报道的准确,Josh Block 确实需要一些教育。Herb Schildt 也是如此。仅举两例。 - Cheers and hth. - Alf
1
@bstar55:通过第四次编辑的Scott写作链接,我在第9项的末尾发现了这句话:“不要在构造函数或析构函数中调用虚函数,因为这样的调用永远不会到达比当前执行构造函数或析构函数更派生的类。”也就是说,因为它们是安全的。这是毫无意义的。但早期的“调用不会做你想做的事情”意味着他特别向那些无法理解动态类型概念的程序员写作,这就是我们所谓的政客民粹主义。唉。 :( - Cheers and hth. - Alf
显示剩余5条评论

8

关于

通常在构造函数中调用虚函数被认为是不良实践,因为子对象中被覆盖的函数尚未被调用。

这并非如此。对于有能力的C++程序员来说,通常不认为从构造函数调用虚函数(除了纯虚函数)是不良实践,因为C++被设计得很好地处理了这个问题。与Java和C#等语言相比,在这些语言中,这可能导致在尚未初始化派生类子对象上调用方法。

请注意,动态类型的动态调整具有运行时成本。

在一个面向极致效率的语言中,“你所不使用的不需要付费”是主要指导原则,这意味着这是一个重要且非常有意义的特性,而不是任意选择。它只存在于一个目的,即支持那些调用。


关于

在这种情况下,类“derived”被标记为final,因此不会再有进一步的子对象存在。因此,虚调用将正确解析(到最终派生类型)。

C++标准保证,在类T的构造执行时刻,动态类型是T。

因此,首先没有解决到不正确的类型问题。


关于

它仍然被认为是不良实践吗?

确实,在final类中声明虚成员函数是不良实践,因为这是毫无意义的。“仍然”也不是非常有意义的。

抱歉,我没有看到虚成员函数是继承而来的。

将成员函数标记为重写或纯虚函数的实现的最佳实践是使用关键字override,而不是将其标记为virtual

因此:

void startFSM() override
{ theFSM_.start(); }

这样可以确保编译错误,如果不是覆盖/实现的话。

7
我必须表示不同意。请参阅Scott Meyers的《Effective C++》中的第9条:“在构造函数和析构函数期间不要调用虚函数”,以及Herb Sutter和Andrei Alexandrescu的《C++编程规范》中的第49条:“避免在构造函数和析构函数中调用虚函数”。 - nosid
7
这听起来像是一种权威论点。我曾在许多场合不同意Scott和Andrei的观点。有时他们同意我,有时则不然。为什么不引用你认为有说服力的理由呢? - Cheers and hth. - Alf
在有能力的C++程序员中,通常不认为这是一种不良实践。我不同意这个句子,因为显然它通常被认为是一种不良实践——无论是否有充分的理由这样做。 - nosid
他完全没有利用职权压制他们。他完全正确地指出这是一种权威上诉。 - Puppy
1
@nosid:哦。你可能是对的,现在有更多的人使用C++,用死记硬背的规则代替了对问题的理解。例如,在他们规则的基础上,Herb和Andrei写道,在构造函数和析构函数中,虚函数不会表现出虚拟的行为,这在技术上是错误的,他们当然知道这是错误的:这是一种过度简化的说法。它适合于死记硬背,而不是理解动态类型的概念。:( - Cheers and hth. - Alf
3
虽然在技术上这一切都是正确的,但在我看来,它并没有解决根本问题:为什么你要在特别禁止多态调用的情况下调用虚方法。这是一种严重的设计问题,在许多情况下可以通过其他方式解决。 - Mark B

0

它可以工作,但为什么startFSM()需要是virtual?在任何情况下,您实际上都不想调用除了derived::startFSM()之外的任何东西,那么为什么要有任何动态绑定呢?如果您想让它调用与动态绑定方法相同的内容,请创建另一个名为startFSM_impl()的非虚函数,并使构造函数和startFSM()都调用它。

如果可以的话,请始终优先选择非虚拟而不是虚拟。


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