2.3.1 Data Member Pointers
数据成员指针类型在ABI层面的基本属性与ptrdiff_t类型一致。
数据成员指针以ptrdiff_t类型表示,指的是该数据成员相对于基类对象的地址点的字节偏移量。
空数据成员指针用偏移量-1表示。
注意,根据[dcl.init],对数据成员指针对象进行“零初始化”会存储一个空指针值。在此表示法下,该值具有非0的位表示(尽管语义上是空指针,但其底层的二进制值不是0)。在大多数现代平台中,数据成员指针是唯一具有这种性质的类型。
对于非空的数据成员指针,其“基类到派生类”(向下转换)或“派生类到基类”(向上转换)的转换可以通过分别加/减派生类中该基类的静态偏移来实现。C++标准不允许成员指针在进行“基类到派生类”或“派生类到基类”的转换时跨越虚基类关系,因此静态偏移量总是已知的。
指向本类成员的数据成员指针总是存储非负偏移量。不幸的是,通过某些转换,非空的数据成员指针也可能会持有负偏移量。如果该值为-1,那么如此以来该成员指针就会被视为空指针。这被认为是此ABI中无法修复的缺陷。对新平台的建议:考虑对数据成员指针使用不同的表示方法,例如将偏移值左移一位,并使用最低位非零来表示非空指针。
在定义良好的代码中,演示此缺陷相对困难。你可以将一个成员指针转换为某个派生类,然后再将其转换回另一个基类;如果第二个基类在内存中排在第一个之后,偏移量将为负。但是,这种转换具有未定义行为,因为成员不再属于该成员指针所属类的基类或派生类。若要展示该缺陷,必须涉及空基类或空数据成员。例如:
1 | struct alignas(2) B1 {}; |
Paw5zx个人理解:点击查看更多
1 | struct alignas(2) B1 {}; |
Member Function Pointers
多种不同的成员函数指针表示方式被使用。其中一种标准表示方式依赖于平台的一些假设,例如:对非虚成员函数的函数指针,其最低位总是为0。
对于那些无法保证这些假设的平台,必须使用一种替代的表示方式。比如,在32位ARM架构中就使用了另一种不同的成员函数指针表示方式,也在此段有描述。
在所有表示方式中,成员函数指针类型的基本ABI属性等价于下面这个结构体,其中fnptr_t是该成员函数所对应的合适的函数指针类型:
1 | struct { |
非虚成员函数的成员函数指针表示为:ptr,其被设置为指向该函数的指针,使用的是基础ABI的函数指针表示方式。
在标准表示中,虚函数的成员函数指针表示为:ptr,其被设置为1加上函数在虚表中的入口偏移量(以字节为单位),然后将该值转换为函数指针,就像通过reinterpret_cast<fnptr_t>(uintfnptr_t(1 + offset)),其中uintfnptr_t是与fnptr_t大小相同的无符号整数类型。
在这两种情况下,adj存储的是在调用之前必须加到this指针上的字节偏移量。
Paw5zx个人理解:点击查看更多
非虚函数的成员函数指针:
ptr:指向该函数的指针adj:以字节为单位,表示调用前必须加到this指针上的偏移量。
虚函数的成员函数指针:
ptr:1加上该函数在虚表中的条目偏移(以字节为单位),然后将其转换为函数指针。(reinterpret_cast<fnptr_t>(uintfnptr_t(1 + offset)))adj:以字节为单位,表示调用前必须加到this指针上的偏移量。
在标准表示中,一个空成员函数指针表示为:ptr,其被设置为空指针。对于空成员函数指针,adj的值未指定。
标准表示法依赖于一些假设,这些假设在大多数平台上都成立:
- 指向非静态成员函数的函数指针的最低位从不会被设置(即始终为0):在大多数平台上,这一点要么始终成立,要么以极小的代价就可以实现。例如,在那些将函数指针仅视为函数首条指令地址的平台上,实现可以确保该地址始终具有足够的对齐,从而使得对于非静态成员函数而言,其最低位为零;这往往是底层体系结构所要求的。
- 一个空函数指针可以与一个虚表偏移量区分开来:在大多数平台上,这总是成立的,因为空函数指针的值是零。
- 指向虚表条目的偏移量永远不会是奇数:在大多数平台上,由于体系结构以字节为寻址单位,并且指针大小为偶数,所以虚表条目的大小也是偶数。
- 只要知道虚表条目的地址以及虚函数的类型,就可以执行虚调用:在大多数平台上,虚表条目等价于一个函数指针,并且该函数指针的类型可以从成员函数指针类型中确定。
Paw5zx个人理解:点击查看更多
只要知道虚表条目的地址以及虚函数的类型,就可以执行虚调用
知道虚函数类型就知道该怎么将其转换为函数指针类型
然而,也有例外情况。例如,在32位ARM架构中,函数指针的最低位用于决定该函数是否以THUMB模式开始。这种平台必须使用一种替代的表示方式。
在32位ARM的表示方式中,存储于adj中的this指针调整值会被左移一位,adj的最低位用于指示ptr是一个函数指针(包括空指针),还是一个虚表条目的偏移量。一个虚成员函数指针会将ptr设置为虚表条目的偏移量,其行为等价于执行reinterpret_cast<fnptr_t>(uintfnptr_t(offset))。一个空的成员函数指针会将ptr设置为一个空函数指针,并且必须确保adj的最低位被清零;adj的高位值则不作规定。
一个成员函数指针在以下条件下被视为null:其ptr等于一个空函数指针,并且(仅当使用32位ARM表示法时)adj的最低位是清零的。
两个成员函数指针相等的条件是:它们都是空指针,或者它们各自的ptr和adj值完全相等。请注意,C++标准并不要求指向同一个虚成员函数的成员指针在比较时必须相等;采用此ABI的实现会让它们相等,但前提是这些成员指针是通过相同的虚表偏移构建的,而在存在多重继承或使用协变返回类型重写函数的情况下,这种偏移可能并不一致。
成员函数指针的“基类到派生类”(向下转换)或“派生类到基类”(向上转换)的转换可以通过在存储的this调整值(即adj)上分别加/减基类在派生类中的静态偏移量来实现。在标准表示中,这仅意味着将该偏移量加到adj上;在32位ARM表示中,该加数必须先左移一位。由于该调整并不影响成员函数指针是否为空,因此在执行转换时可以无条件地进行此加法。
函数调用的执行方式如下:
- 1.将存储的调整值加到this地址上;
- 2.如果成员指针存储的是虚表项的偏移量,则从调整后的this地址中加载虚表,并调用存储偏移量位置处的虚表项;
- 3.否则,调用存储的函数指针。