我想知道,将现有的派生C++类标记为final
以允许进行非虚函数化优化,是否会在使用C ++ 11时改变ABI。我的预期是它不应该产生任何影响,因为我认为这主要是对编译器有关如何优化虚函数的提示。因此我看不出它会改变结构体或虚函数表的大小,但也许我漏掉了什么?
我知道这会改变API,因此进一步派生于此派生类的代码将不再起作用,但在这种情况下,我只关心ABI。
我想知道,将现有的派生C++类标记为final
以允许进行非虚函数化优化,是否会在使用C ++ 11时改变ABI。我的预期是它不应该产生任何影响,因为我认为这主要是对编译器有关如何优化虚函数的提示。因此我看不出它会改变结构体或虚函数表的大小,但也许我漏掉了什么?
我知道这会改变API,因此进一步派生于此派生类的代码将不再起作用,但在这种情况下,我只关心ABI。
函数声明中的final关键字X::f()
表示该声明不能被覆盖,因此所有调用该声明的函数都可以提前绑定(不包括那些调用基类中声明的函数):如果一个虚函数在ABI中被标记为final,则生成的虚表可能与没有final标记的几乎相同类别生成的虚表不兼容:可以假定命名标记为final的声明的虚函数调用是直接的,试图使用虚表条目(在没有final标记的ABI中应该存在)是非法的。
编译器可以利用final保证来减少虚表的大小(有时会增长很多),通过不添加通常会添加的新条目,并且必须符合非final声明的ABI。
条目是针对覆盖函数声明而添加的,而不是针对(固有的)主基类或具有非平凡协变返回类型(在非主基类上协变的返回类型)的情况。
多态继承的简单情况是,派生类从单个多态基类非虚继承而来,这是始终作为主基类的典型情况:多态基类子对象位于开头,派生对象的地址与基类子对象的地址相同,可以直接使用指向任一对象的指针进行虚函数调用,一切都很简单。
这些属性对于派生类是完整对象(不是子对象)、最派生对象或基类的情况都是正确的。(它们是为未知来源的指针在ABI级别上保证的类不变式。)
考虑返回类型不是协变的情况;或:
例如:与*this
具有相同类型的情况下协变;如:
struct B { virtual B *f(); };
struct D : B { virtual D *f(); }; // trivial covariance
D
(子)对象中,B
固有且不变地是主要的:在同一地址上存在B
。因此D*
到B*
转换很简单,所以协变性也很简单:这是一个静态类型问题。this
类型上存在微小差异)struct B1 { virtual void f(); };
struct B2 { virtual void f(); };
struct D : B1, B2 { };
struct DD : D { void f(); }
在定义 D
的 ABI 方面,这并不是有用的。
因此我们可以看到,D
可证明地 需要一个非主多态基类;通常情况下,它将是 D2
;第一个指定的多态基类 (B1
) 将成为主要的。
因此,B2
必须位于非平凡的偏移量上,并且 D
到 B2
的转换是非平凡的:它需要生成的代码。
因此,D
的成员函数的参数不能与 B2
的成员函数的参数相等,因为隐式的 this
并不是平凡可转换的;所以:
D
必须有两个不同的虚表:一个对应于 B1_vtable
,另一个对应于 B2_vtable
(实际上它们被放在一个大的 D
虚表中,但从概念上来说,它们是两个不同的结构)。B2::g
的虚拟成员的虚表条目在 D
中被覆盖,需要两个条目:一个在 D_B2_vtable
中 (它只是一个具有不同值的 B2_vtable
布局),另一个在 D_B1_vtable
中,它是增强的 B1_vtable
:一个 B1_vtable
再加上 D
的新运行时特性条目。由于 D_B1_vtable
是从 B1_vtable
构建的,指向 D_B1_vtable
的指针自然也是指向 B1_vtable
的指针,并且虚指针值相同。
请注意,在理论上,如果通过 B2
基类使所有 D::g()
的虚拟调用负担得起(只要不使用非平凡协变),则可以省略 D_B1_vtable
中 D::g()
条目。
(#) 或者,如果出现非平凡的协变,则不使用“虚拟协变”(涉及虚继承的派生到基类关系中的协变)。
普通(非虚拟)继承就像成员一样简单:
struct VB { virtual void f(); };
struct D : virtual VB { virtual void g(); int dummy; };
struct DD1 : D { void g(); };
struct DD2 : D { void g(); };
struct DDD : DD1, DD2 { };
只有一个DDD::VB
,但在DDD
中有两个可观察到的不同的D
子对象,它们对于D::g()
具有不同的最终覆盖者。无论C++风格的语言(支持虚拟和非虚拟继承语义)是否保证不同的子对象具有不同的地址,DDD::DD1::D
的地址不能与DDD::DD2::D
的地址相同。
因此,在支持基类统一和重复的任何语言中,D
中的VB
的偏移量都无法固定。
在该特定示例中,一个真正的VB
对象(运行时的对象)除了vptr之外没有具体的数据成员,而vptr是一种特殊的标量成员,因为它是类型"不变量"(非const)共享成员:它在构造函数中被固定(完全构造后不变),并且它的语义在基类和派生类之间共享。由于VB
没有不是类型不变量的标量成员,在DDD
中,只要D
的vtable与VB
的vtable匹配,VB
子对象可以是DDD::DD1::D
的覆盖物。
然而,对于具有非不变量标量成员的虚基类,即具有标识符的常规数据成员,即占用不同字节范围的成员:这些“真实”数据成员不能叠加在其他任何东西上。因此,具有数据成员的虚基类子对象(具有一个由C++或任何您正在实现的其他不同C++风格的语言保证的地址不同的成员)必须放置在不同的位置:具有数据成员的虚基类通常具有固有的非平凡偏移量。
所以我们看到,当作为虚基类使用时,“几乎为空”的类(没有数据成员但有vptr的类)是特殊情况:
这意味着当在虚基类中覆盖虚函数时,始终假定需要调整,但在某些情况下将不需要调整。
一个"morally virtual base"是一个基类关系,涉及虚继承(可能还包括非虚继承)。进行从派生到基类的转换,特别是将指向派生类"D"的指针"d"转换为指向基类"B"的指针时,需要进行转换...struct VB { virtual void f(); }; // almost empty
struct D : virtual VB { }; // VB is potential primary
struct Ba { virtual VB * g(); };
struct Da : Ba { // non virtual base, so Ba is inherent primary
D * g(); // virtually covariant: D->VB is morally virtual
};
struct Da : Ba { // non virtual base, so inherent primary
D * g() { return new D; } // VB really is primary in complete D
// so conversion to VB* is trivial here
};
struct VB { virtual void f(); };
struct D : virtual VB { };
struct DD : D
// , virtual VB // imaginary overrider of D inheritance of VB
{
// DD () : VB() { } // implicit definition
};
struct VB { virtual void f(); };
struct D : virtual VB { };
struct DD : D, virtual final VB {
// DD () : VB() { } // implicit definition
};
VB
的虚拟性被冻结了,不能在进一步派生类中使用;VB
的位置被固定。struct DDD : DD {
DD () :
VB() // error: not an almost direct subobject
{ }
};
struct DD2 : D, virtual final VB {
// DD2 () : VB() { } // implicit definition
};
struct Diamond : DD, DD2 // error: no unique final overrider
{ // for ": virtual VB"
};
Diamond::DD::VB
和 Diamond::DD2::VB
变得非法,但是 VB
的虚拟性要求合并,这使得 Diamond
成为了一个矛盾、非法的类定义:没有一个类可以同时从 DD
和 DD2
派生 [类比/例子:就像没有一个有用的类可以直接从 A1
和 A2
派生一样]。struct A1 {
virtual int f() = 0;
};
struct A2 {
virtual unsigned f() = 0;
};
struct UselessAbstract : A1, A2 {
// no possible declaration of f() here
// none of the inherited virtual functions can be overridden
// in UselessAbstract or any derived class
};
这里,UselessAbstract
是抽象的,并且没有派生类,使得该ABC(抽象基类)非常愚蠢,因为任何指向UselessAbstract
的指针都可以成为空指针。
-- 结束模拟/示例
这将提供一种冻结虚继承的方式,以提供具有虚基类的类的有意义的私有继承(否则派生类可以夺取类与其私有基类之间的关系)。
当然,final的这种使用会冻结虚基类在派生类及其进一步派生类中的位置,避免了仅因虚基类的位置未固定而需要额外的vtable条目。
final
关键字不会破坏ABI,然而从现有类中删除它可能会使一些优化无效。例如,请考虑以下内容:// in car.h
struct Vehicle { virtual void honk() { } };
struct Car final : Vehicle { void honk() override { } };
// in car.cpp
// Here, the compiler can assume that no derived class of Car can be passed,
// and so `honk()` can be devirtualized. However, if Car is not final
// anymore, this optimization is invalid.
void foo(Car* car) { car->honk(); }
foo
被单独编译并在共享库中使用,删除final
(从而使用户可以派生自Car
)可能会使优化无效。我不是100%确定,其中一些是推测。final
不会违反ODR吗?此时您将获得未定义的行为。 - Mark Ransomfinal
类中没有引入新的虚方法(只重写父类的方法),那么应该没问题(虚表将与父对象相同,因为它必须能够使用指向父对象的指针进行调用)。如果您引入了虚方法,则编译器确实可以忽略virtual
修饰符并仅生成标准方法,例如:class A {
virtual void f();
};
class B final : public A {
virtual void f(); // <- should be ok
virtual void g(); // <- not ok
};
这个想法是,每次在 C++ 中调用方法 g()
时,您都有一个指向或引用的静态和动态类型为 B
的指针:静态是因为该方法除了 B
及其子类以外不存在,动态是因为 final
确保 B
没有子类。因此,您永远不需要进行虚拟调度来调用 正确的 g()
实现(因为只能有一个),编译器可能(并且应该)不会将其添加到 B
的虚拟表中 - 如果该方法可以被覆盖,则必须这样做。就我所知,这基本上就是 final
关键字存在的全部意义。
A
接口的 B
虚拟表格是正常的,但如果 B
接口与 A
不同,那么情况可能就不太一样了。 - pqnetvirtual
,但编译器可能会聪明地优化掉它(因为它知道它是 final
)。 - pqnet