I was reading the article Member Function Pointers and the Fastest Possible C Delegates from Don Clugston and was experimenting with this stuff myself and was not able to reproduce a case correctly.
Of course, the code from Don Clugston is undefined behaviour.
This is specifically about GCC's representation of member function pointers.
Here's a code snippet from the article about the GCC member function representation (copied as is from the article, not actual code, don't even compile):
// 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
Of course, the standard says nothing about this. Also, GCC ABI might have changed a lot since the article was written. However, I'm not interested in the standard or defined behaviour. I am interested in the current ABI and what the compiler do.
The problem is that I haven't been able to produce a member function pointer that fills the delta
value for me to experiment with it.
I assumed something similar to delta
still exist since the size of a member function pointer is still the size of two pointer. Also, according to my observations, the vtable index trick still apply today.
Here's what I tried:
#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);
}
In all of my example, the last 8 byte of the member function pointer is 0000000000000000
Here's the complete output:
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
How can I produce a member function pointer with a non zero delta on GCC?
CodePudding user response:
I haven't looked at the GCC code, so I'm just doing some guesswork and hypotheses.
The delta is used to adjust the this
pointer. So we have to construct a case where:
MyClass* pThis = ...;
MemberFunctionPointer mfp = ...;
(pThis->*mfp)(); // must adjust this := pThis delta
Why would this
(the this pointer inside the member function) be different from pThis
? This can happen if we call a member function from a different class:
struct B1
{
char c;
};
struct B2
{
char d;
void memfun();
};
struct S : B1, B2
{
void direct();
};
When you do something like
B2 b2;
b2.memfun();
Then we don't have to adjust the this
pointer, this := &b2
. In other words, B2::memfun
expects the this
pointer to point to the (sub)object B2
.
The subobject B2
inside an object of type S
is offset by B1
. Therefore, when we write
S s;
s.memfun();
the compiler has to adjust the address from &s
to &s.d
effectively - it applies a delta.
We can now construct the example that generates a delta in the member function pointer:
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
Note that we can write
m = &S::direct;
(s.*m)(); // calls S::direct, no adjustment, this := &s
this explains why we need to store the delta as part of the member function pointer.
A slight pitfall is the class type used for the member function pointer:
using B2mfp = void(B2::*)();
B2mfp x = &B2::memfun; // x has no delta!
B2 b;
(b.*x)(); // no need to adjust, this := &b
The type of &S::memfun
is actually void(B2::*)()
because that's how inheritance of member functions works: the lookup first searches S
and then its bases. There's no dedicated S::memfun
(no code for it), there's really only B2::memfun
which we can also find with the name/alias S::memfun
.