upcasting和vtables如何协同工作以确保正确的动态绑定?

Aqu*_*irl 21 c++ vtable upcasting dynamic-binding

因此,vtable是由编译器维护的表,其中包含指向该类中的虚函数的函数指针.

将派生类的对象分配给祖先类的对象称为向上转换.

向上转换使用基类指针或引用处理派生类实例/对象; 对象不是"赋值给",这意味着覆盖值ala operator = invocation.
(感谢:Tony D)

现在,如何在运行时知道"哪个"类的虚函数应该被调用?

vtable中的哪个条目指的是应该在运行时调用的"特定"派生类的功能?

Mat*_*son 18

您可以想象(虽然C++规范没有说明这一点)vtable是一个标识符(或一些其他元数据,可用于"查找有关类本身的更多信息")和一系列函数.

所以,如果我们有这样一个类:

class Base
{
  public:
     virtual void func1();
     virtual void func2(int x);
     virtual std::string func3();
     virtual ~Base();
   ... some other stuff we don't care about ... 
};
Run Code Online (Sandbox Code Playgroud)

然后编译器将生成这样的VTable:

struct VTable_Base
{
   int identifier;
   void (*func1)(Base* this);
   void (*func2)(Base* this, int x);
   std::string (*func3)(Base* this); 
   ~Base(Base *this);
};
Run Code Online (Sandbox Code Playgroud)

然后编译器将创建一个内部结构,就像这样(这不可能像C++一样编译,只是为了显示编译器实际执行的操作 - 我称之为Sbase区分实际情况class Base)

struct SBase
{
   VTable_Base* vtable;
   inline void func1(Base* this) { vtable->func1(this); }
   inline void func2(Base* this, int x) { vtable->func2(this, x); }
   inline std::string func3(Base* this) { return vtable->func3(this); }
   inline ~Base(Base* this) { vtable->~Base(this); }
};
Run Code Online (Sandbox Code Playgroud)

它还构建了真正的vtable:

VTable_Base vtable_base = 
{ 
   1234567, &Base::func1, &Base::func2, &Base::func3, &Base::~Base 
};
Run Code Online (Sandbox Code Playgroud)

在构造函数中Base,它将设置vtable = vtable_base;.

当我们添加派生类时,我们覆盖一个函数(默认情况下,析构函数,即使我们没有声明一个):

class Derived : public Base
{
    virtual void func2(int x) override; 
};
Run Code Online (Sandbox Code Playgroud)

编译器现在将构建此结构:

struct VTable_Derived
{
   int identifier;
   void (*func1)(Base* this);
   void (*func2)(Base* this, int x);
   std::string (*func3)(Base* this); 
   ~Base(Derived *this);
};
Run Code Online (Sandbox Code Playgroud)

然后做同样的"结构"建设:

struct SDerived
{
   VTable_Derived* vtable;
   inline void func1(Base* this) { vtable->func1(this); }
   inline void func2(Base* this, int x) { vtable->func2(this, x); }
   inline std::string func3(Base* this) { return vtable->func3(this); }
   inline ~Derived(Derived* this) { vtable->~Derived(this); }
};
Run Code Online (Sandbox Code Playgroud)

当我们Derived直接使用而不是通过Base类时,我们需要这种结构.

(我们依赖编译器链~Derived来调用~Base,就像继承的普通析构函数一样)

最后,我们建立一个实际的vtable:

VTable_Derived vtable_derived = 
{ 
   7654339, &Base::func1, &Derived::func2, &Base::func3, &Derived::~Derived 
};
Run Code Online (Sandbox Code Playgroud)

同样,Derived构造函数将为Dervied::vtable = vtable_derived所有实例设置.

编辑回答在评论的问题:编译器必须小心地将各个部件均VTable_DerivedSDerived,使得它匹配VTable_BaseSBase,这样,当我们有一个指针Base,则Base::vtableBase::funcN()相互匹配Derived::vtableDerived::FuncN.如果不匹配,那么继承将不起作用.

如果添加了新的虚函数Derived,则必须将它们放在继承的虚函数之后Base.

结束编辑.

所以,当我们这样做时:

Base* p = new Derived;

p->func2(); 
Run Code Online (Sandbox Code Playgroud)

代码将查找SBase::Func2,这将使用正确的Derived::func2(因为实际的vtable内部p->vtableVTable_Derived(由与Derived结合使用的构造函数设置new Derived).


Jul*_*ian 5

我将采用与其他答案不同的方法,并尝试仅填补您所学知识中的特定空白,而不必过多地关注细节。我将仅介绍机械原理,以帮助您理解。

因此,vtable是由编译器维护的表,其中包含指向该类中的虚函数的函数指针。

更准确的说法如下:

每个带有虚拟方法的类,包括从带有虚拟方法的类继承的每个类,都有自己的虚拟表。类的虚拟表指向该类特定的虚拟方法,即继承的方法,覆盖的方法或新添加的方法。该类的每个实例都包含一个指向与该类匹配的虚拟表的指针。

向上转换是使用基类指针或引用来处理派生的类实例/对象;(...)

也许更有启发性:

向上转换意味着将对类实例的指针或引用Derived视为对类实例的指针或引用Base。但是,实例本身仍然纯粹是的实例Derived

(当将一个指针“视为指向的指针Base”时,这意味着编译器生成用于处理指向的代码Base。换句话说,编译器和生成的代码并不比对指向的处理要好Base。因此,“视为”的指针将必须指向至少提供与实例相同的接口的对象BaseDerived由于继承的原因,这种情况恰好发生。我们将在下面看到其工作方式。)

此时,我们可以回答您问题的第一个版本。

现在,如何在运行时知道应该调用哪个类的虚拟函数?

假设我们有一个指向的实例的指针Derived。首先,我们向上转换它,因此将其视为的实例的指针Base。然后,我们在转换后的指针上调用虚拟方法。由于编译器知道该方法是虚拟的,因此它知道在实例中查找虚拟表指针。虽然我们将指针当作指向的实例一样对待Base,但实际对象并未更改值,并且其中的虚拟表指针仍指向的虚拟表Derived。因此,在运行时,该方法的地址来自的虚拟表Derived

现在,特定方法可以继承自Base,也可以在中重写Derived。不要紧; 如果是继承的,则虚拟表中的方法指针Derived仅包含与虚拟表中的相应方法指针相同的地址Base。换句话说,两个表都指向该特定方法的相同方法实现。如果重写时,在的虚拟表的方法指针Derived从在虚拟表中的相应方法指针不同Base,所以方法查找上的情况下Derived会发现重写的方法,而在实例查找Base会发现该方法的原始版本- 无论的指向实例的指针是否被视为指向的指针Base或指向的指针Derived

最后,现在应该很容易地解释为什么第二版的问题有点误导了:

vtable中的哪个条目引用“特定”派生类的功能,应该在运行时调用它?

这个问题的前提是vtable查找首先是按方法,然后是按类。反之亦然:首先,实例中的vtable指针用于为正确的类查找vtable。然后,使用该类的vtable查找正确的方法。


dte*_*ech 5

vtable中的哪个条目指的是应该在运行时调用的"特定"派生类的功能?

无,它不是vtable中的条目,而是vtable指针,它是每个对象实例的一部分,用于确定哪个是该特定对象的正确虚函数集.这样,根据指向的实际vtable,从vtable调用"第一虚拟方法"可能导致在同一多态层次结构中为不同类型的对象调用不同的函数.

实现可能会有所不同,但我个人认为最合乎逻辑且最常执行的事情是将vtable指针作为类布局中的第一个元素.这样,您可以取消引用对象的地址,以根据位于该地址中的指针的值确定其类型,因为给定类型的所有对象都将指向同一个vtable,该vtable是为每个vtable创建的具有虚拟方法的对象,这是启用功能以覆盖某些虚拟方法所必需的.

upcasting和vtables如何协同工作以确保正确的动态绑定?

不是严格要求向上倾斜,也不是向下倾斜.请记住,您已经在内存中分配了对象,并且它已经将其vtable指针设置为该类型的正确vtable,这样可以确保它,向下转换不会更改该对象的vtable,它只会更改你操作的指针.

当您想要访问基类中不可用的功能并在派生类中声明时,需要向下转换.但在尝试这样做之前,您必须确保特定对象是或继承声明该​​功能的类型,这是dynamic_cast进入的位置,当您动态转换编译器生成对该vtable条目的检查以及它是否继承在编译时生成的另一个表中请求的类型,如果是,则动态转换成功,否则失败.

您访问对象的指针并不是指要调用的正确的虚函数集,它仅用作衡量vtable中您可以称为开发人员的函数.这就是为什么使用C样式或静态强制转换进行向上转换是安全的,它不执行运行时检查,因为这样你只能将量表限制在基类中可用的函数,这些函数已在派生类中可用,因此有没有错误和伤害的余地.这就是为什么在向下转换时必须始终使用动态强制转换或其他一些仍基于虚拟分派的自定义技术,因为您必须确保对象的关联vtable确实包含您可能调用的额外功能.

否则你会得到未定义的行为,以及那种"坏的",这意味着最有可能发生致命的事情,因为将任意数据解释为要调用的特定签名函数的地址是一个非常大的禁忌.

还要注意,在静态上下文中,即在编译时知道类型是什么时,编译器很可能不会使用vtable来调用虚函数,而是使用直接静态调用甚至内联某些函数,这将使它们成为快多了.在这种情况下,向上转换并使用基类指针而不是实际对象只会减少该优化.