【问题标题】:Call a virtual function inside the constructor using an object-expression使用对象表达式在构造函数中调用虚函数
【发布时间】:2014-08-29 05:12:24
【问题描述】:

代码:

#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(){ }

DEMO

当一个虚函数被直接或间接调用时 构造函数或析构函数,包括在构造过程中或 销毁类的非静态数据成员和对象 调用适用的是对象(称为 x) 下 构造或破坏,调用的函数是最终的 构造函数或析构函数类中的覆盖器,而不是一个 在派生更多的类中覆盖它。如果虚函数调用 使用显式类成员访问 (5.2.5) 和 对象 表达式指的是 x 的完整对象或其中之一 对象的基类子对象,但不是 x 或其基类之一 子对象,行为未定义。

我一直在尝试收到 UB

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

不清楚什么是x的完整对象,其中x是一个对象。和x类型的完整对象一样吗?

【问题讨论】:

  • 您希望 B 构造函数调用重写的 C foo 函数吗?
  • 您希望如何识别 UB?结果可以是任何东西,包括伪装成非 UB。

标签: c++


【解决方案1】:

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

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

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

  • 如果x是一个完整对象,那么x就是x的完整对象。
  • 否则,x 的完整对象是包含x 的(唯一)对象的完整对象。

本质上,您引用的这句话使您的代码中B 的构造函数中的static_cast&lt;C*&gt;(this)-&gt;foo(); 行为未定义,即使正在构造的完整对象是C。该标准实际上在这里提供了一个很好的例子:

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
}

事实上,您已经可以see the undefined behavior show up in this example if you run it:Ideone 的编译器 (GCC) 实际上在 a-&gt;f(); 行上调用了 V::f(),即使指针指向的是完全构造的 A 子对象。

【讨论】:

  • 我不明白这里的评论:f(); // calls V::f, not A::f。为什么有人会在这里想到A::f?它是从哪里来的? A 不在继承中。好像有些错别字。 // calls B::g, not D::g 也是如此。
  • 好的。我得到了它。从 D 构造函数调用 B 构造函数。在此示例中,B 被视为主题。因此是 cmets。
  • 欢迎每日标准问答 Dmitry Fucintv | T.C. ... +1
  • @quantdev 标准也需要爱!
  • @quantdev :D 这对我来说实际上也是一个很好的学习机会——我可能阅读了更多试图回答这些问题的标准。
【解决方案2】:

这有点棘手,我不得不多次编辑帖子(感谢帮助我的人),我会尽量让它简单并参考 N3690:

§12.7.4 状态

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

这就是你在 B 的构造函数中一直在做的事情

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

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

那么标准中还说:

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

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

仍然合法。

终于说到点子上了:

如果虚函数调用使用显式类成员访问,例如 (object.vfunction() 或 object->vfunction()) 并且对象表达式指的是 x 的完整对象或该对象的基类子对象之一,但不是 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

  • 无论如何,如果您从引用该子对象的完整对象的子对象执行此操作(重新阅读标准段落),那么这是未定义的行为

经验法则:don't call virtual functions in your constructors/destructors if you're not really sure you can

如果我有什么问题,请在下面的 cmets 中告诉我,我会修复该帖子。谢谢!

【讨论】:

  • this-&gt;foo();B 的构造函数中很好。毕竟B的构造函数中this的类型是B*
  • 当然可以,但是从 C 调用的时候不行我相信,在这种情况下类型应该是 C
  • 不,仍然是B *。构造函数中this 的类型不会因构造函数的调用方式而神奇地改变。
  • @MarcoA.: 没错,派生类的虚函数在构造之前是不能调用的。因此,“被调用的函数是构造函数或析构函数类中的最终覆盖,而不是在派生更多的类中覆盖它。”但它不是 UB。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-10-05
  • 2012-01-28
  • 1970-01-01
  • 2017-06-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多