我理解普通函数指针包含指向的函数的起始地址,所以当使用普通函数指针时,我们只是跳转到存储的地址。但是指向对象成员函数的指针包含什么?
考虑:
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访问)或普通类成员(实现为具有隐式{{1}的普通函数)获取其值。作为第一个论点。)
那么我的指向成员函数的指针中存储的值是什么?编译器如何按预期工作?
答案 0 :(得分:4)
这当然取决于编译器和目标体系结构,并且有多种单一方法可以实现。但我将描述它在我最常使用的系统上是如何工作的,g ++ for Linux x86_64。
g ++遵循Itanium C++ ABI,它描述了包含虚拟函数在内的各种C ++特性的大部分细节,可以在大多数体系结构的幕后实现。
ABI在2.3节中说明了关于成员函数的指针:
指向成员函数的指针是一对,如下所示:
<强> PTR 强>:
对于非虚函数,此字段是一个简单的函数指针。 ...对于虚函数,它是1加上函数的虚拟表偏移量(以字节为单位),表示为
ptrdiff_t
。零值表示NULL指针,与下面的调整字段值无关。<强> ADJ 强>:
this
所需的调整,表示为ptrdiff_t
。它具有包含这两个成员的类的大小,数据大小和对齐方式。
虚函数的+1到ptr有助于检测函数是否为虚函数,因为对于大多数平台,所有函数指针值和vtable偏移都是偶数。它还确保空成员函数指针具有与任何有效成员函数指针不同的值。
您的班级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
子对象。因此,A
中包含的C
的地址与C
本身的地址不同。
您可能知道,任何时候代码隐式或显式地将(非空)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);
编译器将通过将偏移pf2.adj
添加到地址&c
来实现对成员函数指针的调用,以找到隐含的“this”参数,这很好,因为它将是有效的A*
的{{1}}值为A__func1
。
虚函数调用也是如此,除了反汇编转储显示之外,需要偏移量来找到隐含的“this”参数并找到包含实际功能代码地址的vptr。虚拟情况有一个额外的扭曲,但是它是普通虚拟调用和使用指向成员函数的指针调用所需的:虚拟函数func2
将使用A*
“this”调用参数,因为这是原始重写声明的位置,并且编译器通常不能知道“this”参数是否实际上是任何其他类型。但是覆盖C::func2
的定义需要C*
“this”参数。因此,当最派生的类型为C
时,A
子对象中的vptr将指向一个vtable,其中的条目不是指向C::func2
本身的代码,而是一个小的“ thunk“函数,它只会从”this“参数中减去offset_A_in_C
,然后将控制传递给实际的C::func2
。
答案 1 :(得分:0)
GCC documents,PMF实现为知道如何计算this
的值并进行任何vtable查找的结构。