我正在阅读 Don Clugston 的著名文章 成员函数指针和最快的 C++ 代表,并且自己正在尝试这些东西,但无法正确重现案例。
当然,Don Clugston 的代码是未定义的行为。
这是专门关于 GCC 成员函数指针的表示的。
这是关于 GCC 成员函数表示的文章中的代码片段(按原样从文章中复制,不是实际代码,甚至不编译):
// GNU g++ uses a tricky space optimisation, also adopted by IBM's VisualAge and XLC. struct GnuMFP { union { CODEPTR funcadr; // always even int vtable_index_2; // = vindex*2+1, always odd }; int delta; }; adjustedthis = this + delta if (funcadr & 1) CALL (* ( *delta + (vindex+1)/2) + 4) else CALL funcadr
当然,标准对此只字未提。此外,自本文撰写以来,GCC ABI 可能发生了很大变化。但是,我对标准或定义的行为不感兴趣。我对当前的 ABI 以及编译器的作用感兴趣。
问题是我无法生成一个成员函数指针来填充
delta
值供我进行实验。
我假设类似于
delta
的东西仍然存在,因为成员函数指针的大小仍然是两个指针的大小。另外,根据我的观察,vtable 索引技巧今天仍然适用。
这是我尝试过的:
#include <cstring>
#include <iostream>
#include <iomanip>
void print_pointer(auto const ptr) {
alignas(alignof(ptr)) std::byte memory[sizeof(ptr)];
std::memcpy(memory, std::addressof(ptr), sizeof(ptr));
auto until_newline = int{8};
for (auto const b : memory) {
std::cout << std::hex << std::setfill('0') << std::setw(2) << static_cast<std::uint16_t>(b);
if (--until_newline == 0) {
until_newline = 8;
std::cout << '\n';
}
}
}
// No inheritance, simplest possible
namespace test1 {
struct S {
char a;
void method() const& {
std::cout << "test1 S";
}
};
}
// Simple inheritance, non polymorphic
namespace test2 {
struct B1 { char a; };
struct S : B1 {
char a;
void method() const& {
std::cout << "test1 S";
}
};
}
// Multiple inheritance, non polymorphic
namespace test3 {
struct B1 { char a; };
struct B2 { char a; };
struct S : B1, B2 {
char a;
void method() const& {
std::cout << "test1 S";
}
};
}
// Multiple inheritance, non polymorphic, function in the middle
namespace test4 {
struct B1 { char a; };
struct B2 {
char a;
void method() const& {
std::cout << "test1 S";
}
};
struct S : B1, B2 { char a; };
}
// Simple inheritance, polymorphic
namespace test5 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct S : B1 {
void method() const& override {
std::cout << "test1 S";
}
};
}
// Multiple inheritance, polymorphic, one base only
namespace test6 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct B2 {
char a;
};
struct S : B1, B2 {
void method() const& override {
std::cout << "test1 S";
}
};
}
// Multiple inheritance, polymorphic, two base
namespace test7 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct B2 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct S : B1, B2 {
void method() const& override {
std::cout << "test1 S";
}
};
}
// Simple virtual inheritance, non polymorphic
namespace test8 {
struct B1 { char a; };
struct S : virtual B1 {
void method() const& {
std::cout << "test1 S";
}
};
}
// Simple virtual inheritance, polymorphic
namespace test9 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct S : virtual B1 {
void method() const& override {
std::cout << "test1 S";
}
};
}
// Multiple with one virtual inheritance, one polymorphic
namespace test10 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct B2 {
char a;
};
struct S : B1, virtual B2 {
void method() const& override {
std::cout << "test1 S";
}
};
}
// Multiple with both virtual inheritance, both polymorphic
namespace test11 {
struct B1 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct B2 {
char a;
virtual void method() const& {
std::cout << "test1 S";
}
};
struct S : virtual B1, virtual B2 {
void method() const& override {
std::cout << "test1 S";
}
};
}
int main() {
print_pointer(&test1::S::method);
std::cout << '\n';
print_pointer(&test2::S::method);
std::cout << '\n';
print_pointer(&test3::S::method);
std::cout << '\n';
print_pointer(&test4::S::method);
std::cout << '\n';
print_pointer(&test5::S::method);
std::cout << '\n';
print_pointer(&test6::S::method);
print_pointer(&test6::B1::method);
std::cout << '\n';
print_pointer(&test7::S::method);
print_pointer(&test7::B1::method);
print_pointer(&test7::B2::method);
std::cout << '\n';
print_pointer(&test8::S::method);
std::cout << '\n';
print_pointer(&test9::S::method);
print_pointer(&test9::B1::method);
std::cout << '\n';
print_pointer(&test10::S::method);
print_pointer(&test10::B1::method);
std::cout << '\n';
print_pointer(&test11::S::method);
print_pointer(&test11::B1::method);
print_pointer(&test11::B2::method);
}
在我的所有示例中,成员函数指针的最后 8 个字节是
0000000000000000
这是完整的输出:
b013400000000000 0000000000000000
f013400000000000 0000000000000000
3014400000000000 0000000000000000
d013400000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
1014400000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
0100000000000000 0000000000000000
如何在 GCC 上生成具有非零增量的成员函数指针?
我没有看过GCC代码,所以我只是做一些猜测和假设。
增量用于调整
this
指针。所以我们必须构建一个案例:
MyClass* pThis = ...;
MemberFunctionPointer mfp = ...;
(pThis->*mfp)(); // must adjust this := pThis + delta
为什么
this
(成员函数内的this指针)与pThis
不同?如果我们从不同的类调用成员函数,就会发生这种情况:
struct B1
{
char c;
};
struct B2
{
char d;
void memfun();
};
struct S : B1, B2
{
void direct();
};
当你做类似的事情时
B2 b2;
b2.memfun();
那么我们就不用调整this
指针了,
this := &b2
。换句话说,
B2::memfun
期望
this
指针指向(子)对象
B2
。类型为
B2
的对象内的子对象
S
偏移量为
B1
。因此,当我们写
S s;
s.memfun();
编译器必须有效地将地址从 &s
调整为
&s.d
- 它应用了增量。
using Smfp = void(S::*)();
Smfp m = &S::memfun; // it's really B2::memfun!
S s;
(s.*m)(); // we're calling B2::memfun, therefore we need to adjust this := &s + delta == &s.d
注意我们可以写
m = &S::direct;
(s.*m)(); // calls S::direct, no adjustment, this := &s
这解释了为什么我们需要将 delta 存储为成员函数指针的一部分。
using B2mfp = void(B2::*)();
B2mfp x = &B2::memfun; // x has no delta!
B2 b;
(b.*x)(); // no need to adjust, this := &b
&S::memfun
的类型实际上是
void(B2::*)()
,因为这就是成员函数继承的工作原理:查找首先搜索
S
,然后搜索它的基数。没有专用的
S::memfun
(没有代码),实际上只有
B2::memfun
,我们也可以通过名称/别名
S::memfun
找到它。