指向成员函数的指针如何工作

How do pointers to member functions work?

本文关键字:何工作 工作 指针 成员 函数      更新时间:2023-10-16

我知道普通函数指针包含所指向函数的开始地址,因此当使用普通函数指针时,我们只需跳转到存储的地址。但是指向对象成员函数的指针包含什么?

考虑:

class A
{
public:
int func1(int v) {
std::cout << "fun1";
return v;
}
virtual int func2(int v) {
std::cout << "fun2";
return v;
}
};
int main(int argc, char** argv)
{
A a;
int (A::*pf)(int a) = argc > 2 ? &A::func1 : &A::func2;
static_assert(sizeof(pf) == (sizeof(void*), "Unexpected function size");
return (a.*pf)(argc);
}

在上面的程序中,函数指针可以从虚函数(需要通过 vtable 访问)或普通类成员(作为普通函数实现,隐式this作为第一个参数)获取其值。

那么,存储在指向成员函数的指针中的值是什么,编译器如何使事情按预期工作?

这当然取决于编译器和目标架构,并且有多种方法可以做到这一点。 但是我将描述它在我最常用的系统上是如何工作的,g++ for Linux x86_64。

g++遵循Itanium C++ ABI,它描述了大多数架构可以在幕后实现各种C++功能(包括虚拟功能)的许多细节。

ABI 在第 2.3 节中对指向成员函数的指针进行了说明:

指向成员函数的指针是一对,如下所示:

PTR:

对于非虚函数,此字段是一个简单的函数指针。对于虚函数,它是 1 加上函数的虚拟表偏移量(以字节为单位),表示为ptrdiff_t。值零表示 NULL 指针,与下面的调整字段值无关。

调整

this所需的调整,表示为ptrdiff_t

它具有包含这两个成员的类的大小、数据大小和对齐方式(按该顺序排列)。

虚函数的 +1 到 ptr 有助于检测函数是否是虚拟的,因为对于大多数平台,所有函数指针值和 vtable 偏移量都是偶数。它还确保 null 成员函数指针具有与任何有效成员函数指针不同的值。

您的类A的 vtable/vptr 设置将像这样工作 C 代码:

struct A__virt_funcs {
int (*func2)(A*, int);
};
struct A__vtable {
ptrdiff_t offset_to_top;
const std__typeinfo* typeinfo;
struct A__virt_funcs funcs;
};
struct A {
const struct A__virt_funcs* vptr;
};
int A__func1(struct A*, int v) {
std__operator__ltlt(&std__cout, "fun1");
return v;
}
int A__func2(struct A*, int v) {
std__operator__ltlt(&std__cout, "fun2");
return v;
}
extern const std__typeinfo A__typeinfo;
const struct A__vtable vt_for_A = { 0, &A__typeinfo, { &A__func2 } };
void A__initialize(A* a) {
a->vptr = &vt_for_A.funcs;
}

(是的,实名重整方案需要对函数参数类型做一些事情以允许重载,以及更多事情,因为所涉及的operator<<实际上是函数模板专用化。但这不是重点。

现在让我们看看我为您的main()获得的程序集(带有选项-O0 -fno-stack-protector)。 我的评论已添加。

Dump of assembler code for function main:
// Standard stack adjustment for function setup.
0x00000000004007e6 <+0>: push   %rbp
0x00000000004007e7 <+1>: mov    %rsp,%rbp
0x00000000004007ea <+4>: push   %rbx
0x00000000004007eb <+5>: sub    $0x38,%rsp
// Put argc in the stack at %rbp-0x34.
0x00000000004007ef <+9>: mov    %edi,-0x34(%rbp)
// Put argv in the stack at %rbp-0x40.
0x00000000004007f2 <+12>:    mov    %rsi,-0x40(%rbp)
// Construct "a" on the stack at %rbp-0x20.
// 0x4009c0 is &vt_for_A.funcs.
0x00000000004007f6 <+16>:    mov    $0x4009c0,%esi
0x00000000004007fb <+21>:    mov    %rsi,-0x20(%rbp)
// Check if argc is more than 2.
// In both cases, "pf" will be on the stack at %rbp-0x30.
0x00000000004007ff <+25>:    cmpl   $0x2,-0x34(%rbp)
0x0000000000400803 <+29>:    jle    0x400819 <main+51>
// if (argc <= 2) {
//   Initialize pf to { &A__func2, 0 }.
0x0000000000400805 <+31>:    mov    $0x4008ce,%ecx
0x000000000040080a <+36>:    mov    $0x0,%ebx
0x000000000040080f <+41>:    mov    %rcx,-0x30(%rbp)
0x0000000000400813 <+45>:    mov    %rbx,-0x28(%rbp)
0x0000000000400817 <+49>:    jmp    0x40082b <main+69>
// } else { [argc > 2]
//   Initialize pf to { 1, 0 }.
0x0000000000400819 <+51>:    mov    $0x1,%eax
0x000000000040081e <+56>:    mov    $0x0,%edx
0x0000000000400823 <+61>:    mov    %rax,-0x30(%rbp)
0x0000000000400827 <+65>:    mov    %rdx,-0x28(%rbp)
// }
// Test whether pf.ptr is even or odd:
0x000000000040082b <+69>:    mov    -0x30(%rbp),%rax
0x000000000040082f <+73>:    and    $0x1,%eax
0x0000000000400832 <+76>:    test   %rax,%rax
0x0000000000400835 <+79>:    jne    0x40083d <main+87>
// int (*funcaddr)(A*, int); [will be in %rax]
// if (is_even(pf.ptr)) {
//   Just do:
//   funcaddr = pf.ptr;
0x0000000000400837 <+81>:    mov    -0x30(%rbp),%rax
0x000000000040083b <+85>:    jmp    0x40085c <main+118>
// } else { [is_odd(pf.ptr)]
//   Compute A* a2 = (A*)((char*)&a + pf.adj); [in %rax]
0x000000000040083d <+87>:    mov    -0x28(%rbp),%rax
0x0000000000400841 <+91>:    mov    %rax,%rdx
0x0000000000400844 <+94>:    lea    -0x20(%rbp),%rax
0x0000000000400848 <+98>:    add    %rdx,%rax
//   Compute funcaddr =
//     (int(*)(A*,int)) (((char*)(a2->vptr))[pf.ptr-1]);
0x000000000040084b <+101>:   mov    (%rax),%rax
0x000000000040084e <+104>:   mov    -0x30(%rbp),%rdx
0x0000000000400852 <+108>:   sub    $0x1,%rdx
0x0000000000400856 <+112>:   add    %rdx,%rax
0x0000000000400859 <+115>:   mov    (%rax),%rax
// }
// Compute A* a3 = (A*)((char*)&a + pf.adj); [in %rcx]
0x000000000040085c <+118>:   mov    -0x28(%rbp),%rdx
0x0000000000400860 <+122>:   mov    %rdx,%rcx
0x0000000000400863 <+125>:   lea    -0x20(%rbp),%rdx
0x0000000000400867 <+129>:   add    %rdx,%rcx
// Call int r = (*funcaddr)(a3, argc);
0x000000000040086a <+132>:   mov    -0x34(%rbp),%edx
0x000000000040086d <+135>:   mov    %edx,%esi
0x000000000040086f <+137>:   mov    %rcx,%rdi
0x0000000000400872 <+140>:   callq  *%rax
// Standard stack cleanup for function exit.
0x0000000000400874 <+142>:   add    $0x38,%rsp
0x0000000000400878 <+146>:   pop    %rbx
0x0000000000400879 <+147>:   pop    %rbp
// Return r.
0x000000000040087a <+148>:   retq   
End of assembler dump.

但是,成员函数指针的adj值又是怎么回事呢? 程序集在执行 vtable 查找之前以及在调用函数之前将其添加到a的地址,无论函数是否为虚拟函数。 但是main这两种情况都将其设置为零,因此我们还没有真正看到它的实际应用。

当我们有多个继承时,adj值就会出现。 所以现在假设我们有:

class B
{
public:
virtual void func3() {}
int n;
};
class C : public B, public A
{
public:
int func4(int v) { return v; }
int func2(int v) override { return v; }
};

类型为C的对象的布局包含一个B子对象(其中包含另一个 vptr 和一个int),然后包含一个A子对象。 因此,C中包含的A地址与C本身的地址不同。

您可能知道,任何时候代码隐式或显式地将(非 null)C*指针转换为A*指针时,C++编译器都会通过向地址值添加正确的偏移量来解释这种差异。 C++还允许从指向A的成员函数的指针转换为指向C的成员函数的指针(因为A的任何成员也是C的成员),并且当发生这种情况时(对于非空成员函数指针),需要进行类似的偏移量调整。 因此,如果我们有:

int (A::*pf1)(int) = &A::func1;
int (C::*pf2)(int) = pf1;

引擎盖下的成员函数指针中的值将是pf1 = { &A__func1, 0 };pf2 = { &A__func1, offset_A_in_C };

然后如果我们有

C c;
int n = (c.*pf2)(3);

编译器将通过向地址&c添加偏移量pf2.adj来实现对成员函数指针的调用,以查找隐式的"this"参数,这很好,因为这样它将是A__func1期望的有效A*值。

虚函数调用也是如此,只是如反汇编转储所示,需要偏移量来查找隐式的"this"参数并查找包含实际函数代码地址的 vptr。 虚拟情况有一个额外的转折,但它是普通虚拟调用和使用指向成员函数的指针的调用都需要的:虚拟函数func2将使用A*"this"参数调用,因为这是原始重写声明所在的位置,编译器通常无法知道"this"参数是否实际上是任何其他类型。但是覆盖的定义C::func2需要一个C*的"this"参数。因此,当派生最多的类型是C时,A子对象中的 vptr 将指向一个 vtable,该 vtable 的条目不指向C::func2本身的代码,而是指向一个微小的"thunk"函数,它只做任何事情都不做,只是从 "this" 参数中减去offset_A_in_C,然后将控制权传递给实际的C::func2

GCC 记录了 PMF 作为结构实现的,这些结构知道如何计算this的值并进行任何 vtable 查找。