何时在构造函数中调用虚函数是安全的?

4

我有一些代码,我真的想从构造函数中调用虚方法。我知道这被认为是不安全的,而且我对对象构造也足够了解,也理解为什么会这样,详情请参考此处。我也没有遇到这些问题。目前我的代码正在工作,我认为应该没问题,但我想确认一下。

这是我正在做的事情:

我有一些类层次结构,有一个普通的公共函数,只是像往常一样转发到私有的虚方法。然而,在构造对象时,我确实想调用这个公共方法,因为它正在将所有数据填充到对象中。我将绝对确定这个虚调用来自叶子类,因为在类层次结构的任何其他部分使用这个虚方法根本毫无意义。

所以,在我看来,一旦我进行虚调用,对象创建就应该完成,并且一切都应该没问题。还有什么可能出错吗?我想我必须用一些大注释来标记这部分逻辑,以解释为什么这个逻辑永远不应该移动到任何基类中,即使它看起来可以移动。但除了其他程序员的愚蠢之外,我应该没问题,对吧?


3
只要你所调用该函数的位置是一个叶子类,那么它的行为就会如你所预期的那样。 - Seth Carnegie
永远不要在构造函数或析构函数中调用虚函数 - BЈовић
3个回答

5
在构造函数或析构函数中调用任何非抽象虚函数都是绝对安全的!但是,它的行为可能会让人困惑,因为它可能不会做出预期的操作。当执行一个类的构造函数时,对象的静态和动态类型是构造函数的类型。也就是说,虚函数永远不会分派到更进一步派生类的重写。除此之外,虚分派实际上是有效的:例如,通过基类指针或引用调用虚函数会正确地分派到当前正在构造或销毁的类中的重写。例如(可能包含错别字,因为我目前无法使用此代码):
#include <iostream>
struct A {
    virtual ~A() {}
    virtual void f() { std::cout << "A::f()\n"; }
    void g() { this->f(); }
};
struct B: A {
    B() { this->g(); } // this prints 'B::f()'
    void f() { std::cout << "B::f()\n"; }
};
struct C: B {
    void f() { std::cout << "C::f()\n"; } // not called from B::B()
};

int main() {
    C c;
}

也就是说,如果你不想让虚函数被派发到更深层次的派生函数中,那么在类的构造函数或析构函数中可以直接或间接地调用虚函数。即使给定类中的虚函数是抽象的,只要它被定义了,你甚至也可以这样做。然而,派发到未定义的抽象函数将会导致运行时错误。


他已经知道这个,而且它确实做他认为它会做的事情,因为他知道它是如何工作的,正如他在问题中所陈述的那样。 - Seth Carnegie
@SethCarnegie:好的,我已经编辑了文本。但是,如果他已经知道所有这些,那么问题是:为什么要问这个问题呢? - Dietmar Kühl
据我所读,他只是想知道是否还有其他需要了解的事情:“但除了其他程序员的愚蠢,我应该没问题,对吧?” - Seth Carnegie
2
好的。在这种情况下,他可能想考虑使用C++2011,在那里他可以将类定义为“final”。 - Dietmar Kühl
@DietmarKühl:感谢你指出C++11中的final关键字。我之前没有意识到这个关键字已经(不是故意的双关语)被添加进去了。我会马上看看明天能不能用它,并且也会尝试重新思考类层次结构,使其更加安全。 - LiKao

3

当构造函数被调用时,类设置为该类的实例,但不是派生类的实例。您不能从基类构造函数中调用派生类的虚函数。在到达最终派生类的构造函数时,所有的虚函数都应该是安全的。

如果您希望确保没有人会进行错误的调用,请在基类中定义虚函数,并在其被调用时使用assert和/或throw异常。


如果你将函数定义为纯虚函数,那么这个问题会解决吗?(或者说,会发生什么?我以前从未尝试过) - Seth Carnegie
@SethCarnegie,好问题。我认为如果您调用纯虚函数,即如果构造函数调用Foo()并调用虚函数Bar(),则没有定义的行为。 - Mark Ransom
@SethCarnegie:我之前在其他代码中遇到过这个问题,记得那时程序崩溃了。我想在这种情况下会产生一些特殊的错误,至少对于GCC来说是这样,但我不确定是否100%正确(也可能只是SIGSEGV)。标准可能只将其定义为UB,我猜测没有查找。 - LiKao
它取决于编译器/运行时,但值得一提的是,g++和clang++将打印“pure virtual method called”并调用abort()。 - smparkes

2
规则并不是说你必须在一个叶子类中,而是要认识到当你从Foo::Foo(..)中进行成员调用时,对象恰好是一个Foo,即使它正在变成一个Bar(假设Foo是从Bar派生而来,并且您正在构造一个Bar实例)。这是100%可靠的。
否则,成员函数是虚拟的这一事实并不是很重要。还有其他陷阱也会发生在非虚拟函数中:如果您在构造函数中在对象完全构造之前调用了一个假定对象已经完全构造的虚拟或非虚拟方法,则也会出现问题。这些只是难以确定的情况,因为不仅您调用的函数必须没问题,它调用的所有函数也必须没问题。
听起来您没有问题,只是这种情况容易出现错误。

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