为什么构造函数中对虚拟成员函数的调用是非虚拟调用?
假设我有两个 C++ 类:
class A
{
public:
A() { fn(); }
virtual void fn() { _n = 1; }
int getn() { return _n; }
protected:
int _n;
};
class B : public A
{
public:
B() : A() {}
virtual void fn() { _n = 2; }
};
如果我编写以下代码:
int main()
{
B b;
int n = b.getn();
}
人们可能期望 n
设置为 2。
结果,n
设置为 1。为什么?
Suppose I have two C++ classes:
class A
{
public:
A() { fn(); }
virtual void fn() { _n = 1; }
int getn() { return _n; }
protected:
int _n;
};
class B : public A
{
public:
B() : A() {}
virtual void fn() { _n = 2; }
};
If I write the following code:
int main()
{
B b;
int n = b.getn();
}
One might expect that n
is set to 2.
It turns out that n
is set to 1. Why?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(13)
解决您的问题的一种方法是使用工厂方法来创建对象。
One solution to your problem is using factory methods to create your object.
正如已经指出的,这些对象是在构造时自下而上创建的。 当构造基对象时,派生对象还不存在,因此虚函数重写无法工作。
但是,如果您的 getter 返回常量,或者可以用静态成员函数表示,则可以通过使用静态多态性而不是虚函数的多态 getter 来解决这个问题,此示例使用 CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)。
通过使用静态多态性,基类知道要调用哪个类的 getter,因为信息是在编译时提供的。
As has been pointed out, the objects are created base-down upon construction. When the base object is being constructed, the derived object does not exist yet, so a virtual function override cannot work.
However, this can be solved with polymorphic getters that use static polymorphism instead of virtual functions if your getters return constants, or otherwise can be expressed in a static member function, This example uses CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).
With the use of static polymorphism, the base class knows which class' getter to call as the information is provided at compile-time.
C++ 标准 (ISO/IEC 14882-2014 ) 说的是:
因此,不要从尝试调用正在构造或销毁的对象的构造函数或析构函数调用
虚拟
函数,因为构造顺序从基类到派生开始,并且析构函数的顺序从派生到基类开始。因此,尝试从构造中的基类调用派生类函数是危险的。同样,对象以与构造相反的顺序被销毁,因此尝试从析构函数调用更派生类中的函数可能会访问已经存在的资源。已被释放。
The C++ Standard (ISO/IEC 14882-2014) say's:
So, Don't invoke
virtual
functions from constructors or destructors that attempts to call into the object under construction or destruction, Because the order of construction starts from base to derived and the order of destructors starts from derived to base class.So, attempting to call a derived class function from a base class under construction is dangerous.Similarly, an object is destroyed in reverse order from construction, so attempting to call a function in a more derived class from a destructor may access resources that have already been released.
你知道 Windows 资源管理器的崩溃错误吗?! “纯虚函数调用...”
同样的问题...
因为没有实现函数 pureVitualFunction() 并且在构造函数中调用该函数,所以程序将崩溃。
Do you know the crash error from Windows explorer?! "Pure virtual function call ..."
Same problem ...
Because there is no implemetation for the function pureVitualFunction() and the function is called in the constructor the program will crash.
A::A()
的作用是:请注意,vptr 是每个对象,而 vtable 是每个类,
B::B()
的作用是:B 的 vtable,不再是 A 的 vtable。
因此,A::A() 保证调用 A::fn() 而不是 B::fn()。
注释
可能需要修改多个 vptr。
What
A::A()
does is:of A. Note that vptr is per object, while vtable is per class
What
B::B()
does is:vtable of B, not to vtable of A any more.
Therefore, A::A() is guaranteed to call A::fn() rather than B::fn().
Notes
might need to modify more than one vptr.
vtable 由编译器创建。
类对象有一个指向其 vtable 的指针。 当它开始生命时,该vtable指针指向vtable
的基类。 在构造函数代码的末尾,编译器生成代码来重新指向vtable指针
到该类的实际 vtable。 这确保调用虚函数的构造函数代码调用
这些函数的基类实现,而不是类中的重写。
The vtables are created by the compiler.
A class object has a pointer to its vtable. When it starts life, that vtable pointer points to the vtable
of the base class. At the end of the constructor code, the compiler generates code to re-point the vtable pointer
to the actual vtable for the class. This ensures that constructor code that calls virtual functions calls the
base class implementations of those functions, not the override in the class.
作为补充,调用尚未完成构造的对象的虚函数也会面临同样的问题。
例如,在一个对象的构造函数中启动一个新线程,并将该对象传递给新线程,如果新线程在该对象构造完成之前调用该对象的虚函数,将会导致意外的结果。
例如:
这将输出:
As a supplement, calling a virtual function of an object that has not yet completed construction will face the same problem.
For example, start a new thread in the constructor of an object, and pass the object to the new thread, if the new thread calling the virtual function of that object before the object completed construction will cause unexpected result.
For example:
This will output:
为了回答运行该代码时会发生什么/为什么,我通过编译它
g++ -ggdb main.cc
,并使用 gdb 逐步执行。main.cc:
在
main
处设置断点,然后单步执行 B(),打印this
ptr,单步执行 A()(基本构造函数):显示
this
最初指向在 0x7fffffffde80 处的堆栈上构造的派生 B objb
。 下一步是进入基 A() 构造函数,并且this
变为相同地址的A * const
,这是有意义的,因为基 A 正好位于 B 的开头目的。 但它仍然没有被构造:再一步:
_n已经被初始化,并且它的虚函数表指针包含
virtual void A::fn()
的地址:所以这是完全有道理的下一步在给定活动
this
和_vptr.A
的情况下,通过 this->fn() 执行 A::fn()。 又一步,我们回到 B() 构造函数:基础 A 已经构造完毕。 请注意,存储在虚拟函数表指针中的地址已更改为派生类 B 的 vtable。因此,对 fn() 的调用将通过 this->fn() 选择派生类重写 B::fn(),给定活动
this
和_vptr.A
(在 B() 中取消对 B::fn() 调用的注释以查看此内容。)再次检查 _vptr.A 中存储的 1 个地址显示它现在指向派生类重写:通过查看此示例,并通过查看具有 3 级继承的示例,可以看出,当编译器向下构造基本子对象时,
this* 的类型
和 _vptr.A 中的相应地址发生变化以反映当前正在构造的子对象, - 因此它会指向最派生的类型。 因此,我们希望从 ctor 内部调用的虚拟函数能够选择该级别的函数,即,结果与非虚拟相同。对于 dtor 也是如此,但相反。 在构造成员时,this
成为成员的 ptr,因此它们也可以正确调用为它们定义的任何虚拟函数。To answer what happens/why when you run that code, I compiled it via
g++ -ggdb main.cc
, and stepped through with gdb.main.cc:
Setting a break point at
main
, then stepping into B(), printing thethis
ptr, taking a step into A() (base constructor):shows that
this
initially points at the derived B objb
being constructed on the stack at 0x7fffffffde80. The next step is into the base A() ctor andthis
becomesA * const
to the same address, which makes sense as the base A is right in the start of B object. but it still hasn't been constructed:One more step:
_n has been initialized, and it's virtual function table pointer contains the address of
virtual void A::fn()
:So it makes perfect sense that the next step executes A::fn() via this->fn() given the active
this
and_vptr.A
. Another step and we're back in B() ctor:The base A has been constructed. Note that address stored in the virtual function table pointer has changed to the vtable for derived class B. And so a call to fn() would select the derived class override B::fn() via this->fn() given the active
this
and_vptr.A
(un-comment call to B::fn() in B() to see this.) Again examining 1 address stored in _vptr.A shows it now points to the derived class override:By looking at this example, and by looking at one with a 3 level inheritance, it appears that as the compiler descends to construct the base sub-objects, the type of
this*
and the corresponding address in_vptr.A
change to reflect the current sub-object being constructed, - so it gets left pointing to the most derived type's. So we would expect virtual functions called from within ctors to choose the function for that level, i.e., same result as if they were non-virtual.. Likewise for dtors but in reverse. Andthis
becomes a ptr to member while members are being constructed so they also properly call any virtual functions that are defined for them.我刚刚在程序中遇到了这个错误。
我有这样的想法:如果该方法在构造函数中被标记为纯虚拟会发生什么?
还有……有趣的事情! 您首先会收到编译器的警告:
以及来自 ld 的错误!
这是完全不合逻辑的,你只得到编译器的警告!
I just had this error in a program.
And I had this thinking : what happens if the method is marked as pure virtual in the constructor?
And... funny thing! You first get a warining by the compiler :
And an error from ld!
This is totally illogic that you get just a warning from the compiler!
从构造函数或析构函数调用虚函数是危险的,应尽可能避免。 所有 C++ 实现都应该调用当前构造函数中层次结构级别定义的函数版本,而不是进一步调用。
C++ FAQ Lite 在漂亮的第 23.7 节中介绍了这一点很好的细节。 我建议阅读该内容(以及常见问题解答的其余部分)以进行后续操作。
摘抄:
编辑更正了大多数(感谢litb)
Calling virtual functions from a constructor or destructor is dangerous and should be avoided whenever possible. All C++ implementations should call the version of the function defined at the level of the hierarchy in the current constructor and no further.
The C++ FAQ Lite covers this in section 23.7 in pretty good detail. I suggest reading that (and the rest of the FAQ) for a followup.
Excerpt:
EDIT Corrected Most to All (thanks litb)
在大多数面向对象语言中,从构造函数调用多态函数会导致灾难。 不同的语言在遇到这种情况时会有不同的表现。
基本问题是,在所有语言中,基类型必须在派生类型之前构造。 现在的问题是从构造函数调用多态方法意味着什么。 您期望它的表现如何? 有两种方法:在 Base 级别调用方法(C++ 风格)或在层次结构底部的未构造对象上调用多态方法(Java 风格)。
在 C++ 中,基类将在进入其自己的构造之前构建其虚拟方法表的版本。 此时,对虚拟方法的调用将最终调用该方法的基本版本,或者生成一个名为的纯虚拟方法,以防它在层次结构的该级别上没有实现。 完全构建 Base 后,编译器将开始构建 Derived 类,并将重写方法指针以指向层次结构的下一层中的实现。
在 Java 中,编译器将在构造的第一步、进入 Base 构造函数或 Derived 构造函数之前构建等效的虚拟表。 其含义是不同的(而且根据我的喜好更危险)。 如果基类构造函数调用在派生类中重写的方法,则该调用实际上将在派生级别处理,调用未构造对象上的方法,从而产生意外结果。 在构造函数块内初始化的派生类的所有属性尚未初始化,包括“final”属性。 具有在类级别定义的默认值的元素将具有该值。
如您所见,调用多态(C++ 术语中的虚拟)方法是常见的错误来源。 在 C++ 中,至少你可以保证它永远不会在尚未构造的对象上调用方法......
Calling a polymorphic function from a constructor is a recipe for disaster in most OO languages. Different languages will perform differently when this situation is encountered.
The basic problem is that in all languages the Base type(s) must be constructed previous to the Derived type. Now, the problem is what does it mean to call a polymorphic method from the constructor. What do you expect it to behave like? There are two approaches: call the method at the Base level (C++ style) or call the polymorphic method on an unconstructed object at the bottom of the hierarchy (Java way).
In C++ the Base class will build its version of the virtual method table prior to entering its own construction. At this point a call to the virtual method will end up calling the Base version of the method or producing a pure virtual method called in case it has no implementation at that level of the hierarchy. After the Base has been fully constructed, the compiler will start building the Derived class, and it will override the method pointers to point to the implementations in the next level of the hierarchy.
In Java, the compiler will build the virtual table equivalent at the very first step of construction, prior to entering the Base constructor or Derived constructor. The implications are different (and to my likings more dangerous). If the base class constructor calls a method that is overriden in the derived class the call will actually be handled at the derived level calling a method on an unconstructed object, yielding unexpected results. All attributes of the derived class that are initialized inside the constructor block are yet uninitialized, including 'final' attributes. Elements that have a default value defined at the class level will have that value.
As you see, calling a polymorphic (virtual in C++ terminology) methods is a common source of errors. In C++, at least you have the guarantee that it will never call a method on a yet unconstructed object...
原因是 C++ 对象的构造就像洋葱一样,从内到外。 基类在派生类之前构造。 因此,在制造 B 之前,必须先制造 A。 当 A 的构造函数被调用时,它还不是 B,因此虚函数表仍然有 A 的 fn() 副本的条目。
The reason is that C++ objects are constructed like onions, from the inside out. Base classes are constructed before derived classes. So, before a B can be made, an A must be made. When A's constructor is called, it's not a B yet, so the virtual function table still has the entry for A's copy of fn().
C++ FAQ Lite 很好地涵盖了这一点:
The C++ FAQ Lite Covers this pretty well: