虚函数表:底层算法

8

我的理解是,如果我有一个类Cat,并且这个类有一个虚函数speak(),然后分别有Lion和HouseCat两个子类,那么就会有一个vtable,将speak()映射到每个子类的正确实现。因此,调用

cat.speak()

编译为

cat.vtable[0]()

也就是说,查找vtable的位置0并调用该位置上的函数指针。
我的问题是: 多重继承时会发生什么?
我们添加一个类Pet。Pet有虚函数speak()和eat()。HouseCat继承自Pet,而Lion没有。现在,我需要确保...
pet.eat()

编译成

pet.vtable[1]()

需要将vtable[0]改为speak(),Pet.eat需要是slot 1。这是因为cat.speak()需要访问vtable中的slot 0,如果对于一只HouseCat来说,slot 0 恰好是eat,那么会出现严重错误。

编译器如何确保vtable索引匹配?


1
简短的回答是编译器确保这一点,因为那是编译器的工作。这就是它应该做的事情。所以它会这样做。当超类被单独实例化时,编译器会为其创建单独的虚函数表;当它作为子类的一部分实例化时,编译器也会为其创建单独的虚函数表,并在实例化时将适当的虚函数表分配给超类实例。 - Sam Varshavchik
1
如果您想了解所有细节,可以在Itanium C++ ABI中找到一种实现方式的描述。 - Alan Stokes
2
该对象可能包含多个指向虚函数表的指针 - 实际上,每个具有虚函数的基类都会有一个。 - Alan Stokes
作为对其他评论的跟进,事实上可以有多个vptr,这意味着eat不一定必须在位置1。Pet可以有一个指定{eat, speak}的vtable,而Cat则保留一个指定{speak}的vtable。 - aschepler
@aschepler 在大多数情况下,vtable 中的顺序遵循类定义中的顺序。 - curiousguy
显示剩余2条评论
1个回答

1

规范没有设定任何内容,但通常编译器会为每个直接非虚基类生成一个vtable,再加上一个派生类的vtable - 然后第一个基类和派生类的vtable将被合并。

更具体地说,编译器在构建类时生成的内容:

  • Cat

    [vptr | Cat fields]
     [0]: speak()
    
  • Pet

    [vptr | Pet fields]
     [0]: eat()
    
  • Lion

    [vptr | Cat fields | Lion fields]
     [0]: speak()
    
  • HouseCat

    [vptr | Cat fields | vptr | Pet fields | HouseCat fields]
     [0]: speak()        [0]: eat()
    

编译器在调用/转换时生成的内容(变量名称为静态类型名称):

  • cat.speak()
    • obj[0][0]() - 对于Cat、Lion和HouseCat的“Cat”部分有效
  • pet.eat()
    • obj[0][0]() - 对于Pet和HouseCat的“Pet”部分有效
  • lion.speak()
    • obj[0][0]() - 对于Lion有效
  • houseCat.speak()
    • obj[0][0]() - 对于HouseCat的“Cat”部分有效
  • houseCat.eat()
    • obj[Cat size][0]() - 对于HouseCat的“Pet”部分有效
  • (Cat)houseCat
    • obj
  • (Pet)houseCat
    • obj + Cat size

所以我想让你困惑的关键是(1)可能存在多个虚表和(2)向上转换实际上可能会返回不同的地址。


如果存在第一个基类的[virtual]表和派生类的[virtual]表,则它们将被合并。通常称为主要基类的“第一个[非虚拟]基类”。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接