【问题标题】:Is it bad practice to call a virtual function from constructor of a class that is marked final从标记为 final 的类的构造函数中调用虚函数是不好的做法吗
【发布时间】:2014-08-21 03:10:52
【问题描述】:

通常从构造函数调用虚函数被认为是不好的做法,因为子对象中的覆盖函数不会被调用,因为对象尚未构造。

但是,请考虑以下类:

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 是一个非常有用的问题,我不知道从派生的构造函数调用虚函数在被覆盖的行为中有不同的行为。这种行为在 ISO 标准中是否存在,并且实际上会因编译器而异?另外请注意,虚拟方法是私有的,您可以公开继承。
  • 如果一年后你决定 derived 不应该是 final 会发生什么?
  • re “通常从构造函数调用虚函数被认为是不好的做法,因为子对象中的覆盖函数将不会被调用,因为对象尚未构造。”,如果你的同伴中是这样的话学生或同事,那么他们需要接受教育。 AFAIK 这不是 C++ 程序员的共同观点。但如果是这样,那么整个 C++ 程序员都需要接受教育。
  • base中的什么方法通常调用startFSM?这似乎是相关的,因此我们可以做出充分有根据的决定。

标签: c++ c++11 constructor virtual-functions dynamictype


【解决方案1】:

这仍然被认为是不好的做法,因为这种情况几乎总是表明设计不好。您必须对代码进行注释以解释为什么这在这种情况下有效。

T.C. 的上述评论强化了这被认为是不良做法的原因之一。

如果一年后,您决定派生 毕竟不应该是最终的?

也就是说,在上面的示例中,该模式将毫无问题地工作。这是因为最派生类型的构造函数是调用虚函数的构造函数。当基类的构造函数调用解析为子类型实现的虚函数时,就会出现此问题。在 C++ 中,不会调用这样的函数,因为在基类构造期间,这样的调用永远不会转到比当前执行的构造函数或析构函数更派生的类。从本质上讲,您最终会出现出乎意料的行为。

编辑:

所有(正确的/非错误的)C++ 实现都必须调用在当前构造函数的层次结构级别定义的函数版本,而不是进一步。

C++ FAQ Lite 在第 23.7 节中非常详细地介绍了这一点。

Scott Meyers 还在 Effective C++ 第 9 条中讨论了从构造函数和析构函数调用虚函数的一般问题

【讨论】:

  • @bstsr55 这个问题是否取决于您使用的编译器的实现,还是在 ISO 标准中?
  • -1 “这仍然被认为是不好的做法,因为这种情况几乎总是表明设计不好”调用首先不是不好的做法(但在最终类中声明一个虚拟成员函数毫无意义,因此确实是糟糕的实践和糟糕的设计)。
  • @Alf:最终派生类继承了该成员函数上的virtual修饰符......将成员函数声明为virtual可能没有意义,因为无论基类是否声明它都是virtual ,但记录它是虚拟的是很常见的。当然,用新的 override 关键字记录它会更好。
  • @BenVoigt:谢谢,我没看到。我会使用override 而不是virtual 来证明它是一个实现。带着对好的代码的期望阅读......
  • @bstar55:在第 4 次编辑的 Scott 写作链接中,我在第 9 项的末尾发现了这一点:“不要在构造或销毁期间调用虚函数,因为这样的调用永远不会到达比当前执行的构造函数或析构函数更多的派生类。” IE。 因为它们很安全。那是没有意义的。但是前面的“电话不会像你想的那样做”意味着他是专门为无法理解动态类型概念的程序员写的,当政客这样做时,我们称之为民粹主义。啊。 :(
【解决方案2】:

关于

通常从构造函数调用虚函数被认为是不好的做法,因为子对象中的覆盖函数不会被调用,因为对象尚未构造。

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

注意动态类型的动态调整是有运行时开销的。

在一种以最终效率为导向的语言中,以“不为不使用的东西付费”为主要指导原则,这意味着它是一个重要且非常有意的特性,而不是任意选择。它只存在一个目的。即支持那些电话。


关于

在这种情况下,派生类被标记为最终类,因此不能存在更多子对象。因此,虚拟调用将正确解析(到最派生的类型)。

C++ 标准保证在构造一个类T时,动态类型是T

因此,首先解决错误类型没有问题。


关于

仍然被认为是不好的做法吗?

final 类中声明成员函数virtual 确实是一种不好的做法,因为那毫无意义。“仍然”也不是很有意义。

对不起,我没有看到虚成员函数是这样继承的。

将成员函数标记为纯虚拟的覆盖或实现的最佳做法是使用关键字override,而不是将其标记为virtual

因此:

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

如果它不是覆盖/实现,这将确保编译错误。

【讨论】:

  • 我不同意。参见 Scott Meyers 的 Effective C++ 中的 Item 9: Never call virtual functions during construction or destroyItem 49: Avoid call virtual functions in constructors and destructors i> 在 Herb Sutter 和 Andrei Alexandrescu 撰写的 C++ 编码标准 中。
  • @nosid:这听起来像是一个权威论点。我曾多次不同意斯科特和安德烈的观点。有时他们同意我的意见,有时不同意。为什么不引用你认为有说服力的理由。
  • “在有能力的 C++ 程序员中,这通常不被视为不好的做法。”我不同意这句话,因为显然它通常被认为是不好的做法——不管这样做是否有充分的理由。
  • @nosid:哦。你可能是对的,现在使用 C++ 的人要多得多,死记硬背的规则取代了对问题的理解。例如,在the rationale for their rule Herb 和 Andrei 中写道,虚函数在构造函数和析构函数中不表现为虚函数,这在技术上是错误的,他们当然知道这是错误的:这是一种过度简化。它适合死记硬背,而不是理解动态类型的概念。 :(
  • 这在技术上都是正确的,但在我看来,它并没有解决这里的根本问题:why are you calling a virtual method in a context that specifically inhibits the polymorphic aspect of the call。这是一种强烈的设计气味,在许多情况下可以通过其他方式解决。
【解决方案3】:

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

如果可以的话,总是更喜欢非虚拟而不是虚拟。

【讨论】:

    猜你喜欢
    • 2021-02-18
    • 2013-03-31
    • 1970-01-01
    • 2014-08-15
    • 2015-07-10
    • 2015-01-17
    相关资源
    最近更新 更多