理解vptr中的多重继承? [英] understanding vptr in multiple inheritance?

查看:120
本文介绍了理解vptr中的多重继承?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图理解书中有效的c ++中的语句。以下是多重继承的继承图。





/ p>

现在这本书说vptr需要每个类中有独立的内存。它也使下面的语句



上图中的奇怪是,只有三个vptr,即使涉及四个类。实现可以自由地生成四个vptr,如果他们喜欢,但三个足够(原来是B和D可以共享一个vptr),大多数实现利用这个机会减少编译器生成的开销。 p>

我看不到任何理由为什么在vptr的每个类中都需要单独的内存。我理解vptr是继承自基类,任何可能是继承类型。如果我们假设它显示带有继承的vptr的结果内存结构,他们如何能做出


B和D可以共享一个vptr


有人可以澄清一点关于vptr的多重继承吗?



    $ b
  • 如果上面是真的,为什么B和D可以共享vptr?


解决方案

你的问题很有趣,但是我担心你的目标太大是第一个问题,所以我会在几个步骤回答,如果你不介意:)



免责声明:我不是编译器编写者,虽然我一定研究这个主题,我的话应该谨慎。会有我不准确。我不是那么精通RTTI。此外,由于这不是标准,我所描述的是可能性。



1。如何实现继承?



注意:我将省略对齐问题,它们只是意味着一些填充可以包含在块之间



让我们暂且不说虚拟方法,现在,关注如何实现继承。



事实是继承和组合有很多共同点:

  struct B {int t; int u; }; 
struct C {B b; int v; int w; };
struct D:B {int v; int w; };

看起来像:

  B:
+ ----- + ----- +
| t | u |
+ ----- + ----- +

C:
+ ----- + ----- + ----- + ----- +
| B | v | w |
+ ----- + ----- + ----- + ----- +

D:
+ ----- ----- + ----- + ----- +
| B | v | w |
+ ----- + ----- + ----- + ----- +

$ b $



然而,这意味着,多重继承很容易弄清楚:

  struct A {int r; int s; }; 
struct M:A,B {int v; int w; };

M:
+ ----- + ----- + ----- + ----- + ----- + ----- +
| A | B | v | w |
+ ----- + ----- + ----- + ----- + ----- + ----- +

使用这些图,让我们看看当将指针投射到基指针时会发生什么:

  M * pm = new M(); 
A * pa = pm; //指向M的一个子部分
B * pb = pm; //指向M
的B子部分

使用我们的上图:

  M:
+ ----- + ----- + ----- + ----- + --- + ----- +
| A | B | v | w |
+ ----- + ----- + ----- + ----- + ----- + ----- +
^ ^
pm pb
pa

事实上 pb pm 稍有不同,是由编译器自动通过指针运算处理。



2。如何实现虚拟继承?



虚拟继承很棘手:您需要确保单个 V (对于虚拟)对象将被所有其他子对象共享。让我们定义一个简单的钻石继承。

  struct V {int t; }; 
struct B:virtual V {int u; };
struct C:virtual V {int v; };
struct D:B,C {int w; };

我将省略该表示,并集中精力确保在 D 对象, B C 子部分共享同一个子对象。


  1. 请记住,类大小应该是常数

  2. 请记住, / b>

    因此,找到的解决方案很简单:

  3. code> B 和 C 只保留指向 V 和:




    • 如果您构建独立的 B ,构造函数将分配如果您构建 B $ V c>作为 D 的一部分, B 子部分将期望 D 构造函数将指针传递到 V
    • 的位置


    C 显然。



    ,优化允许构造函数在对象中保留 V 的空间,因为 D 不会从 B C ,尽管我们还没有虚拟方法。 p>

      B:(and C is similar)
    + ----- + ----- +
    | V * | u |
    + ----- + ----- +

    D:
    + ----- + ----- + ----- + ----- + ----- + ----- +
    | B | C | w | A |
    + ----- + ----- + ----- + ----- + ----- + ----- +

    现在注意从 B A 比简单的指针算术稍微棘手:您需要遵循 B 中的指针,而不是简单的指针算术。



    有一个更糟的情况,虽然,up-casting。如果我给你一个 A 的指针,你怎么知道如何回到 B



    在这种情况下,魔法由 dynamic_cast 执行,但这需要一些支持(即信息)存储在某处。这是所谓的 RTTI (运行时类型信息)。 dynamic_cast 将首先确定 A D 通过一些魔术,然后查询D的运行时信息知道 D B 子对象的存储位置。

    如果我们在没有 B 子对象的情况下,它将返回0(指针形式)或抛出 bad_cast 例外(参考表单)。



    3。如何实现虚拟方法?



    一般而言,虚拟方法通过每个类实现一个v表(即一个函数指针表) v-ptr到每个对象的此表。这不是唯一可能的实现,并且已经证明其他可以更快,然而它既简单又具有可预测的开销(在存储器和调度速度方面)。



    如果我们使用一个简单的基类对象,使用一个虚方法:

      struct B {virtual foo ; }; 

    对于计算机,没有成员方法这样的东西,所以实际上你有: p>

      struct B {VTable * vptr; }; 

    void Bfoo(B * b);

    struct BVTable {RTTI * rtti; void(* foo)(B *); };

    当从 B p>

      struct D:B {virtual foo(); virtual bar(); }; 

    现在有两个虚方法,一个覆盖 B :: foo ,另一个是全新的。计算机表示形式类似于:

      struct D {VTable * vptr; }; //单表,甚至两种方法

    void Dfoo(D * d); void Dbar(D * d);

    struct DVTable {RTTI * rtti; void(* foo)(D *); void(* foo)(B *); };

    请注意 BVTable 和<$ c $ (因为我们在 bar 之前放置 foo )?非常重要!

      D * d = / ** / 
    B * b = d; // noop,no needfor arithmetic

    b-> foo();

    让我们将调用转换为机器中的 foo 语言(有点):

      // 1.获取vptr 
    void * vptr = b; // noop,它存储在B的第一个字节

    // 2.获取指向foo函数的指针
    void(* foo)(B *)= vptr [1]; // 0 for for RTTI

    // 3.应用foo
    (* foo)(b);

    这些vptrs是由对象的构造函数初始化的,当执行 D ,这里是发生了什么:




    • D :: D / code>调用 B :: B()首先是要初始化它的子部分

    • B :: B()初始化 vptr 以指向其vtable,然后返回

    • D :: D()初始化 vptr ,指向其vtable,覆盖B



      • 因此, vptr 这里指向D的vtable,因此 foo 应用是D的。 B 它完全透明。



        这里B和D共享相同的vptr! >



        4。多继承中的虚拟表



        不幸的是,这种共享并不总是可能。



        正如我们已经看到的,在虚拟继承的情况下,共享项目在最终完成对象中奇怪地定位。因此它有自己的vptr。这是 1



        其次,在多继承的情况下,第一个基准与完整的对象对齐,但第二个基准不能(它们都需要空间用于其数据),因此它不能共享其vptr。

        第三个​​基本 与整个对象对齐,因此为我们提供了相同的布局在简单继承的情况下(相同的优化机会)。那是 3



        很简单,不是?


        I am trying to make sense of the statement in book effective c++. Following is the inheritance diagram for multiple inheritance.

        Now the book says separate memory in each class is required for vptr. Also it makes following statement

        An oddity in the above diagram is that there are only three vptrs even though four classes are involved. Implementations are free to generate four vptrs if they like, but three suffice (it turns out that B and D can share a vptr), and most implementations take advantage of this opportunity to reduce the compiler-generated overhead.

        I could not see any reason why there is requirement of separate memory in each class for vptr. I had an understanding that vptr is inherited from base class whatever may be the inheritance type. If we assume that it shown resultant memory structure with inherited vptr how can they make the statement that

        B and D can share a vptr

        Can somebody please clarify a bit about vptr in multiple inheritance?

        • Do we need separate vptr in each class ?
        • Also if above is true why B and D can share vptr ?

        解决方案

        Your question is interesting, however I fear that you are aiming too big as a first question, so I will answer in several steps, if you don't mind :)

        Disclaimer: I am no compiler writer, and though I have certainly studied the subject, my word should be taken with caution. There will me inaccuracies. And I am not that well versed in RTTI. Also, since this is not standard, what I describe are possibilities.

        1. How to implement inheritance ?

        Note: I will leave out alignment issues, they just mean that some padding could be included between the blocks

        Let's leave it out virtual methods, for now, and concentrate on how inheritance is implemented, down below.

        The truth is that inheritance and composition share a lot:

        struct B { int t; int u; };
        struct C { B b; int v; int w; };
        struct D: B { int v; int w; };
        

        Are going to look like:

        B:
        +-----+-----+
        |  t  |  u  |
        +-----+-----+
        
        C:
        +-----+-----+-----+-----+
        |     B     |  v  |  w  |
        +-----+-----+-----+-----+
        
        D:
        +-----+-----+-----+-----+
        |     B     |  v  |  w  |
        +-----+-----+-----+-----+
        

        Shocking isn't it :) ?

        This means, however, than multiple inheritance is quite simple to figure out:

        struct A { int r; int s; };
        struct M: A, B { int v; int w; };
        
        M:
        +-----+-----+-----+-----+-----+-----+
        |     A     |     B     |  v  |  w  |
        +-----+-----+-----+-----+-----+-----+
        

        Using these diagrams, let's see what happens when casting a derived pointer to a base pointer:

        M* pm = new M();
        A* pa = pm; // points to the A subpart of M
        B* pb = pm; // points to the B subpart of M
        

        Using our previous diagram:

        M:
        +-----+-----+-----+-----+-----+-----+
        |     A     |     B     |  v  |  w  |
        +-----+-----+-----+-----+-----+-----+
        ^           ^
        pm          pb
        pa
        

        The fact that the address of pb is slightly different from that of pm is handled through pointer arithmetic automatically for you by the compiler.

        2. How to implement virtual inheritance ?

        Virtual inheritance is tricky: you need to ensure that a single V (for virtual) object will be shared by all the other subobjects. Let's define a simple diamond inheritance.

        struct V { int t; };
        struct B: virtual V { int u; };
        struct C: virtual V { int v; };
        struct D: B, C { int w; };
        

        I'll leave out the representation, and concentrate on ensuring that in a D object, both the B and C subparts share the same subobject. How can it be done ?

        1. Remember that a class size should be constant
        2. Remember that when designed, neither B nor C can foresee whether they will be used together or not

        The solution that has been found is therefore simple: B and C only reserve space for a pointer to V, and:

        • if you build a stand-alone B, the constructor will allocate a V on the heap, which will be handled automatically
        • if you build B as part of a D, the B subpart will expect the D constructor to pass the pointer to the location of V

        And idem for C, obviously.

        In D, an optimization allow the constructor to reserve space for V right in the object, because D does not inherit virtually from either B or C, giving the diagram you have shown (though we don't have yet virtual methods).

        B:  (and C is similar)
        +-----+-----+
        |  V* |  u  |
        +-----+-----+
        
        D:
        +-----+-----+-----+-----+-----+-----+
        |     B     |     C     |  w  |  A  |
        +-----+-----+-----+-----+-----+-----+
        

        Remark now that casting from B to A is slightly trickier than simple pointer arithmetic: you need follow the pointer in B rather than simple pointer arithmetic.

        There is a worse case though, up-casting. If I give you a pointer to A how do you know how to get back to B ?

        In this case, the magic is performed by dynamic_cast, but this require some support (ie, information) stored somewhere. This is the so called RTTI (Run-Time Type Information). dynamic_cast will first determine that A is part of a D through some magic, then query D's runtime information to know where within D the B subobject is stored.

        If we were in case where there is no B subobject, it would either return 0 (pointer form) or throw a bad_cast exception (reference form).

        3. How to implement virtual methods ?

        In general virtual methods are implemented through a v-table (ie, a table of pointer to functions) per class, and v-ptr to this table per-object. This is not the sole possible implementation, and it has been demonstrated that others could be faster, however it is both simple and with a predictable overhead (both in term of memory and dispatch speed).

        If we take a simple base class object, with a virtual method:

        struct B { virtual foo(); };
        

        For the computer, there is no such things as member methods, so in fact you have:

        struct B { VTable* vptr; };
        
        void Bfoo(B* b);
        
        struct BVTable { RTTI* rtti; void (*foo)(B*); };
        

        When you derive from B:

        struct D: B { virtual foo(); virtual bar(); };
        

        You now have two virtual methods, one overrides B::foo, the other is brand new. The computer representation is akin to:

        struct D { VTable* vptr; }; // single table, even for two methods
        
        void Dfoo(D* d); void Dbar(D* d);
        
        struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
        

        Note how BVTable and DVTable are so similar (since we put foo before bar) ? It's important!

        D* d = /**/;
        B* b = d; // noop, no needfor arithmetic
        
        b->foo();
        

        Let's translate the call to foo in machine language (somewhat):

        // 1. get the vptr
        void* vptr = b; // noop, it's stored at the first byte of B
        
        // 2. get the pointer to foo function
        void (*foo)(B*) = vptr[1]; // 0 is for RTTI
        
        // 3. apply foo
        (*foo)(b);
        

        Those vptrs are initialized by the constructors of the objects, when executing the constructor of D, here is what happened:

        • D::D() calls B::B() first and foremost, to initiliaze its subparts
        • B::B() initialize vptr to point to its vtable, then returns
        • D::D() initialize vptr to point to its vtable, overriding B's

        Therefore, vptr here pointed to D's vtable, and thus the foo applied was D's. For B it was completely transparent.

        Here B and D share the same vptr!

        4. Virtual tables in multi-inheritance

        Unfortunately this sharing is not always possible.

        First, as we have seen, in the case of virtual inheritance, the "shared" item is positionned oddly in the final complete object. It therefore has its own vptr. That's 1.

        Second, in case of multi-inheritance, the first base is aligned with the complete object, but the second base cannot be (they both need space for their data), therefore it cannot share its vptr. That's 2.

        Third, the first base is aligned with the complete object, thus offering us the same layout that in the case of simple inheritance (the same optimization opportunity). That's 3.

        Quite simple, no ?

        这篇关于理解vptr中的多重继承?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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