使用对象表达式在构造函数中调用虚函数

5

代码:

#include <iostream>

using std::cout;
using std::endl;

struct A
{
    virtual void foo()
    {
        cout << "A" << endl;
    }

    A(){ }
};

struct B : A
{
    B();
    virtual void foo()
    {
        cout << "B" << endl;
    }
};

B b;

B::B()
{
    b.foo();
    foo();
}  

struct C : B
{
    virtual void foo()
    {
        cout << "C" << endl;
    }

    C() : B(){ }      
};

C c;

int main(){ }

演示

当从构造函数或析构函数中直接或间接调用虚函数,包括在类的非静态数据成员的构造或析构期间,并且被调用的函数适用于正在构建或销毁的对象(称其为 x),所调用的函数是构造函数或析构函数类中的最终覆盖函数,而不是更派生类中覆盖它的函数。如果虚函数调用使用显式类成员访问(5.2.5),并且对象表达式是指向 x 的完整对象或该对象的其中一个基类子对象,但不是 x 或其基类子对象之一,则行为未定义。

我一直在尝试收到有关下列问题的信息:

如果虚函数调用使用显式类成员访问(5.2.5),并且对象表达式是指向 x 的完整对象...

什么是 x 的完整对象意思不明确,其中 x 是一个对象。它是否与类型为 x 的完整对象相同?


你想让B构造函数调用被重写的C foo函数吗? - Anthony Raimondo
3
你如何期望识别UB?结果可能是任何东西,包括伪装成非UB。 - rici
2个回答

8

§1.8 [intro.object]/p2-3:

对象可以包含其他对象,称为 子对象。子对象可以是 成员子对象 (9.2),基类子对象 (第 10 章) 或数组元素。不作为任何其他对象的子对象的对象称为 完全对象

对于每个对象 x,都存在一个称为 x完全对象,可按以下方式确定:

  • 如果 x 是完全对象,则 xx 的完全对象。
  • 否则,x 的完全对象是包含 x 的 (唯一) 对象的完全对象。

简言之,即使正在构建的完全对象是 C,在 B 的构造函数中执行 static_cast<C*>(this)->foo(); 也会导致您的代码出现未定义行为。标准实际上在此处提供了一个相当好的例子:

struct V {
    virtual void f();
    virtual void g();
};
struct A : virtual V {
    virtual void f();
};
struct B : virtual V {
    virtual void g();
    B(V*, A*);
};

struct D : A, B {
    virtual void f();
    virtual void g();
    D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
    f();    // calls V::f, not A::f
    g();    // calls B::g, not D::g
    v->g(); // v is base of B, the call is well-defined, calls B::g
    a->f(); // undefined behavior, a’s type not a base of B
}

事实上,如果你运行它,你已经可以看到这个例子中的未定义行为了:Ideone的编译器(GCC)实际上在a->f();行调用了V::f(),即使指针是指向完全构造的A子对象。


好的,我明白了。D 构造函数中调用了 B 构造函数。在这个例子中,B 被视为主题。因此有注释。 - Nawaz
@Nawaz,那不是我的评论 :) 我认为它们是基于被构建的完整对象是一个D - T.C.
2
欢迎参加每日标准问答,Dmitry Fucintv | T.C. ... +1 - quantdev
1
@quantdev标准也需要关爱! - Marco A.
1
@quantdev :D 这对我来说是一个很好的学习机会——我可能会在尝试回答这些问题时读更多的标准。 - T.C.
显示剩余3条评论

2

这有点棘手,我不得不多次编辑这篇文章(感谢帮助我的人),我会尝试简单地解释并参考N3690:

§12.7.4规定

成员函数,包括虚函数(10.3),可以在构造或析构期间调用(12.6.2)。

这就是你在B的构造函数中所做的事情。

B::B()
{
    b.foo(); // virtual
    foo(); // virtual
}  

目前这是完全合法的。在第二个函数调用中隐含使用的this指针总是指向正在构造的对象。

然后标准还说:

当从构造函数直接或间接地调用虚函数并且调用适用于正在构造或销毁的对象(称其为x)时,被调用的函数是构造函数或析构函数类中的最终覆盖函数,而不是一个更派生类中覆盖它的函数(因此忽略该函数的更派生版本)。

所以虚表并没有完全遍历,如你所想,而是停止到构造函数类版本的虚函数(参见http://www.parashift.com/c%2B%2B-faq-lite/calling-virtuals-from-ctors.html)。

仍然是合法的。

最后来到你的重点:

如果虚函数调用使用显式类成员访问,例如(object.vfunction()或object->vfunction()),并且对象表达式引用x的完整对象或该对象的一个基类子对象之一,但不是正在构造的对象或其基类子对象之一(即不是正在构造的对象或其基类子对象),则行为未定义。

要理解这个句子,我们首先需要了解x的完整对象是什么意思:

§1.8.2

对象可以包含其他对象,称为子对象。子对象可以是成员子对象(9.2)、基类子对象(第10条)或数组元素。不是任何其他对象的子对象的对象称为完整对象。

对于每个对象x,都有一些称为x的完整对象的对象,如下所示确定:

— 如果x是完整对象,则x是x的完整对象。

— 否则,x的完整对象是包含x的(唯一)对象的完整对象

如果你将上面的段落与前面的段落结合起来,你会发现不能调用引用基类的“完整类型”的虚函数(即尚未构造的派生对象)或拥有该成员或数组元素的对象。

如果你要在B的构造函数中明确引用C:

B::B() {
    static_cast<C*>(this)->foo(); // Refers to the complete object of B, i.e. C
}

struct C : B
{
    C() : B(){ }
}

如果你这样做,那么就会产生未定义的行为。

直觉上(或多或少)的原因是:

  • 在构造函数中调用虚拟函数或成员函数是允许的,而对于虚拟函数而言,它将“停止虚拟层次遍历”到该对象,并调用其版本的函数(请参见http://www.parashift.com/c%2B%2B-faq-lite/calling-virtuals-from-ctors.html)。

  • 但是,如果你从一个子对象引用到该子对象的完整对象,则是未定义的行为(请重新阅读标准段落)。

经验法则:如果你不确定能否在构造函数/析构函数中调用虚拟函数,请不要这样做。

如果我有什么错误,请在下面的评论中告诉我,我会修正文章。谢谢!


B 的构造函数中,this->foo(); 是完全可以的。毕竟,在 B 的构造函数中,this 的类型是 B* - T.C.
可以,但是我认为从C中调用时不太好,这种情况下类型应该是C。 - Marco A.
不,它仍然是 B *。构造函数内部的 this 类型并不会根据构造函数被调用的方式而自动更改。 - T.C.
无论你如何做,都不能从基类构造函数中调用派生类的虚函数。请参考:http://www.parashift.com/c++-faq/using-this-in-ctors.html - Marco A.
@rici 谢谢你帮我理解。我一直都弄错了。我会修改帖子的。 - Marco A.
显示剩余4条评论

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