C ++构造函数:为什么这个虚函数调用不安全? [英] C++ constructors: why is this virtual function call not safe?

查看:102
本文介绍了C ++构造函数:为什么这个虚函数调用不安全?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是从C ++ 11标准sec 12.7.4。


  1. 正文中最后一句是什么意思?

  2. B :: B 未定义的最后一个方法调用? Shoudn't它只是调用 a.A :: f




4在构建期间,成员函数,包括虚函数(10.3)或破坏(12.6.2)。当虚拟函数
直接或间接从构造函数或
析构函数调用时,包括在构建或销毁
类的非静态数据成员期间,调用
apply是对象(调用它x)正在构造或销毁,
调用的函数是构造函数或
析构函数类中的最终重写,而不是一个重写类。
如果虚函数调用使用显式类成员访问
(5.2.5),对象表达式引用x
的完整对象或该对象的基类子对象之一,而不是x或其
基类子对象之一,行为是未定义的。 [示例:

  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(); //调用V :: f,而不是A :: f
g(); //调用B :: g,而不是D :: g
v-> g(); // v是B的基址,调用是很好定义的,调用B :: g
a-> f(); //未定义的行为,a的类型不是B的基础
}



<



解决方案

标准的这一部分只是告诉你, 对象 J 其基类层次结构包含多个继承,并且您当前位于某个基本子对象的构造函数内 H ,那么您只能使用 H 及其直接和间接基本子对象的多态性。



例如,考虑这个继承图(箭头指向从派生类到基类)



>



假设我们正在构造一个类型为 J 的大型对象。我们当前正在执行 H 的构造函数。在 H 的构造函数中,您可以享受红色椭圆内部的子层次结构的典型构造函数限制多态性。例如,您可以调用 B 类型的base subobject的虚拟函数,多态行为将在圆圈子层次中按预期工作(如预期意味着多态行为将在层次结构中低至 H ,但不低于)。您还可以调用 A E X 和其他子对象落在红色椭圆内。



但是,如果你以某种方式获得对层次结构外的椭圆形,并尝试使用多态性,行为变得不确定。例如,如果你以某种方式从 H 的构造函数中访问 G 子对象,并尝试调用虚函数 G - 行为未定义。关于从 H的构造函数中调用 D I



获得这种访问外部子层次结构的唯一方法是如果某人以某种方式通过指针/引用 G 将子对象放入 H 的构造函数中。因此,在标准文本中引用显式类成员访问(虽然似乎过多)。



标准包括进入示例来说明这条规则是如何包容的。在上面的图中,基础子对象 X 由椭圆内的子层次和椭圆外的子层次共享。标准说,可以从 H 的构造函数中调用 X 子对象的虚函数。

请注意,即使构建 D G I 子对象在 H 开始之前已经完成。






本规范的根源导致实现多态机制的实际考虑。在实际实现中,将VMT指针作为数据字段引入层次结构中最基本的多态类的对象布局中。派生类不会引入自己的VMT指针,他们只是为基类(以及可能更长的VMT)引入的指针提供自己的特定



看看标准的例子。类 A 源自类 V 。这意味着 A 的VMT指针物理上属于 V 子对象。由 V 引入的对虚函数的所有调用都通过 V 引入的VMT指针调度。也就是说每当您调用

  pointer_to_A-> f 

它实际上被翻译成

  V * v_subobject =(V *)pointer_to_A; // go to V 
vmt = v_subobject-> vmt_ptr; //检索表
vmt [index_for_f](); //通过表调用

然而,在标准的例子中,非常相同 V 子对象也嵌入到 B 中。为了使构造器限制的多态性正常工作,编译器将指向 B 的VMT到VMT指针中的指针存储在 V (因为 B 的构造函数是活动的 V 子对象必须作为<$



如果此刻您试图呼叫

  a-> f(); //如示例中的

上述算法将会找到 B 的VMT指针存储在其 V 子对象中,并将尝试通过VMT调用 f() 。这显然没有任何意义。也就是说通过 B 的VMT分派虚拟方法 A 没有任何意义。行为未定义。



这是相当简单的验证与实际的实验。让我们将 f 添加到 B 并执行此操作

  #include< iostream> 

struct V {
virtual void f(){std :: cout< V std :: endl; }
};

struct A:virtual V {
virtual void f(){std :: cout< A<< std :: endl; }
};

struct B:virtual V {
virtual void f(){std :: cout< B< std :: endl; }
B(V *,A *);
};

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

B :: B(V * v,A * a){
a-> f(); //什么`f()`在这里被调用?
}

int main(){
D d;
}

您期望 A :: f 在这里调用?我试过几个编译器,他们实际上调用 B :: f !同时,这个指针值 B :: f 在这种调用中接收完全是假的。



http://ideone.com/Ua332



这发生正是由于我上面描述的原因(大多数编译器实现多态性我上面描述的方式)。



可能会注意到,在这个特定的例子中,它实际上是虚拟这种不寻常的行为。是的,正好是因为 V 子对象在 A B subobjects。很可能没有虚拟继承,行为将是更可预测的。然而,语言规范显然决定只是绘制在我的图中绘制的方式:当你构造 H 时,你不允许走出沙箱 c> H 的子层次结构,无论使用什么继承类型。


This is from the C++11 standard sec 12.7.4. This is rather confusing.

  1. What does the last sentence in the text mean exactly?
  2. Why is the last method call in B::B undefined? Shoudn't it just call a.A::f?

4 Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class. If the virtual function call uses an explicit class member access (5.2.5) and the object expression refers to the complete object of x or one of that object’s base class subobjects but not x or one of its base class subobjects, the behavior is undefined. [ Example:

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
}

—end example ]

解决方案

That portion of the standard is simply telling you that when you are constructing some "large" object J whose base class hierarchy includes multiple inheritance, and you are currently sitting inside the constructor of some base subobject H, then you are only allowed to use polymorphism of H and its direct and indirect base subobjects. You are not allowed to use any polymorphism outside that subhierarchy.

For example, consider this inheritance diagram (arrows point from derived classes to base classes)

Let's say we are constructing a "large" object of type J. And we are currently executing the constructor of class H. Inside the constructor of H you are allowed to enjoy typical constructor-restricted polymorphism of the subhierarchy inside the red oval. For example, you can call virtual functions of base subobject of type B, and the polymorphic behavior will work as expected inside the circled subhierarchy ("as expected" means that the polymorphic behavior will go as low as H in the hierarchy, but no lower). You can also call virtual functions of A, E, X and other subobjects that fall inside the red oval.

However, if you somehow gain access to the hierarchy outside the oval and attempt to use polymorphism there, the behavior becomes undefined. For example, if you somehow gain access to G subobject from the constructor of H and attempt to call a virtual function of G - the behavior is undefined. The same can be said about calling virtual functions of D and I from the constructor of H.

The only way to obtain such access to the "outside" subhierarchy is if someone somehow passed a pointer/reference to G subobject into the constructor of H. Hence the reference to "explicit class member access" in the standard text (although it seems to be excessive).

The standard includes virtual inheritance into the example to demonstrate how inclusive this rule is. In the above diagram base subobject X is shared by both the subhierarchy inside the oval and subhierarchy outside the oval. The standard says that it is OK to call virtual functions of X subobject from the constructor of H.

Note that this restriction applies even if the construction of D, G and I subobjects has been finished before the construction of H began.


The roots of this specification lead to practical consideration of implementing polymorphic mechanism. In practical implementations the VMT pointer is introduced as a data field into the object layout of the most basic polymorphic classes in the hierarchy. Derived classes don't introduce their own VMT pointers, they simply provide their own specific values for the pointers introduced by the base classes (and, possibly, longer VMTs).

Take a look at the example from the standard. The class A is derived from class V. This means that the VMT pointer of A physically belongs to V subobject. All calls to virtual functions introduced by V are dispatched through VMT pointer introduced by V. I.e. whenever you call

pointer_to_A->f();

it is actually translated into

V *v_subobject = (V *) pointer_to_A; // go to V
vmt = v_subobject->vmt_ptr;          // retrieve the table
vmt[index_for_f]();                  // call through the table

However, in the example from the standard the very same V subobject is also embedded into B. In order to make the constructor-restricted polymorphism work correctly, the compiler will place a pointer to B's VMT into VMT pointer stored in V (because while B's constructor is active V subobject has to act as part of B).

If at this moment you somehow attempt to call

a->f(); // as in the example

the above algorithm will find B's VMT pointer stored in its V subobject and will attempt to call f() through that VMT. This obviously makes no sense at all. I.e. having virtual methods of A dispatched through B's VMT makes no sense. The behavior is undefined.

This is rather simple to verify with practical experiment. Let's add its own version of f to B and do this

#include <iostream>

struct V {
  virtual void f() { std::cout << "V" << std::endl; }
};

struct A : virtual V {
  virtual void f() { std::cout << "A" << std::endl; }
};

struct B : virtual V {
  virtual void f() { std::cout << "B" << std::endl; }
  B(V*, A*);
};

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

B::B(V* v, A* a) {
  a->f(); // What `f()` is called here???
}

int main() {
  D d;
}

You expect A::f to be called here? I tried several compilers, an all of them actually call B::f! Meanwhile, the this pointer value B::f receives in such call is completely bogus.

http://ideone.com/Ua332

This happens exactly for the reasons I described above (most compilers implement polymorphism the way I described above). This is the reason the language describes such calls as undefined.

One might note that in this specific example it is actually the virtual inheritance that leads to this unusual behavior. Yes, it happens exactly because the V subobject is shared between A and B subobjects. It is quite possible that without virtual inheritance the behavior would be much more predictable. However, the language specification apparently decided to just draw line the the way it is drawn in my diagram: when you are constructing H you are not allowed to step out of the "sandbox" of H's subhierarchy regardless of what inheritance type is used.

这篇关于C ++构造函数:为什么这个虚函数调用不安全?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆