Dav*_*fal 220 c++ constructor overriding virtual-functions
假设我有两个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; }
};
Run Code Online (Sandbox Code Playgroud)
如果我写下面的代码:
int main()
{
B b;
int n = b.getn();
}
Run Code Online (Sandbox Code Playgroud)
人们可能期望将n其设置为2.
事实证明,n设置为1.为什么?
Jar*_*Par 204
从构造函数或析构函数调用虚函数是危险的,应尽可能避免使用.所有C++实现都应调用当前构造函数中层次结构级别定义的函数版本,不再进一步调用.
在C++ FAQ精简版包括这在相当不错的细节部分23.7.我建议您阅读(以及常见问题的其余部分)以获得后续信息.
摘抄:
[...]在构造函数中,虚拟调用机制被禁用,因为尚未发生从派生类的重写.对象是从基础构建的,"在派生之前的基础".
[...]
销毁是在"基类之前的派生类"完成的,因此虚函数的行为与构造函数相同:只使用本地定义 - 并且不会调用重写函数以避免触及对象的(现在销毁的)派生类部分.
编辑纠正最重要(感谢litb)
Dav*_*eas 80
在大多数OO语言中,从构造函数调用多态函数是一种灾难.遇到这种情况时,不同语言的表现会有所不同.
基本问题是,在所有语言中,Base类型必须在Derived类型之前构建.现在,问题是从构造函数中调用多态方法意味着什么.你期望它的表现如何?有两种方法:在Base级别调用方法(C++样式)或在层次结构底部的未构造对象上调用多态方法(Java方式).
在C++中,Base类将在进入自己的构造之前构建其虚拟方法表的版本.此时,对virtual方法的调用将最终调用方法的Base版本,或者生成一个调用的纯虚方法,以防它在层次结构的该级别没有实现.在完全构建Base之后,编译器将开始构建Derived类,并且它将覆盖方法指针以指向层次结构的下一级中的实现.
class Base {
public:
Base() { f(); }
virtual void f() { std::cout << "Base" << std::endl; }
};
class Derived : public Base
{
public:
Derived() : Base() {}
virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run
Run Code Online (Sandbox Code Playgroud)
在Java中,编译器将在构造的第一步,在进入Base构造函数或Derived构造函数之前构建等效的虚拟表.影响是不同的(我的喜欢更危险).如果基类构造函数调用在派生类中重写的方法,则实际将在派生级别处理调用未构造对象上的方法,从而产生意外结果.在构造函数块内初始化的派生类的所有属性尚未初始化,包括"final"属性.具有在类级别定义的默认值的元素将具有该值.
public class Base {
public Base() { polymorphic(); }
public void polymorphic() {
System.out.println( "Base" );
}
}
public class Derived extends Base
{
final int x;
public Derived( int value ) {
x = value;
polymorphic();
}
public void polymorphic() {
System.out.println( "Derived: " + x );
}
public static void main( String args[] ) {
Derived d = new Derived( 5 );
}
}
// outputs: Derived 0
// Derived 5
// ... so much for final attributes never changing :P
Run Code Online (Sandbox Code Playgroud)
如您所见,调用多态(C++术语中的虚拟)方法是常见的错误来源.在C++中,至少你可以保证它永远不会在一个尚未构造的对象上调用方法......
Dav*_*fal 56
原因是C++对象从内到外构造成洋葱.在派生类之前构造超类.因此,在制作B之前,必须制作A. 当调用A的构造函数时,它还不是B,因此虚函数表仍然具有A的fn()副本的条目.
Aar*_*paa 23
在C++ FAQ精简版封面这还算不错:
本质上,在对基类构造函数的调用期间,该对象还不是派生类型,因此调用基类型的虚函数实现而不是派生类型.
Tob*_*ias 13
您的问题的一个解决方案是使用工厂方法来创建对象.
class Object
{
public:
virtual void afterConstruction() {}
// ...
};
template< class C >
C* factoryNew()
{
C* pObject = new C();
pObject->afterConstruction();
return pObject;
}
class MyClass : public Object
{
public:
virtual void afterConstruction()
{
// do something.
}
// ...
};
MyClass* pMyObject = factoryNew();
正如已经指出的,这些对象是在构造时自下而上创建的。当构造基对象时,派生对象还不存在,因此虚函数重写无法工作。
但是,如果您的 getter 返回常量,或者可以用静态成员函数表示,则可以使用使用静态多态性而不是虚函数的多态 getter 来解决此问题,此示例使用 CRTP ( https://en.wikipedia.org/wiki /Curiously_recurring_template_pattern)。
template<typename DerivedClass>
class Base
{
public:
inline Base() :
foo(DerivedClass::getFoo())
{}
inline int fooSq() {
return foo * foo;
}
const int foo;
};
class A : public Base<A>
{
public:
inline static int getFoo() { return 1; }
};
class B : public Base<B>
{
public:
inline static int getFoo() { return 2; }
};
class C : public Base<C>
{
public:
inline static int getFoo() { return 3; }
};
int main()
{
A a;
B b;
C c;
std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;
return 0;
}
Run Code Online (Sandbox Code Playgroud)
通过使用静态多态性,基类知道要调用哪个类的 getter,因为信息是在编译时提供的。
| 归档时间: |
|
| 查看次数: |
91098 次 |
| 最近记录: |