简体   繁体   中英

When is the delta used in a member function pointer on GCC?

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 

Live example

How can I produce a member function pointer with a non zero delta on GCC?

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM