GCC 什么时候在成员函数指针中使用增量?

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

我正在阅读 Don Clugston 的文章 Member Function Pointers and the Fastest Possible C++ Delegates,并且自己正在试验这些东西,但无法正确重现案例。

当然,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 

Live example

如何在 GCC 上生成具有非零增量的成员函数指针?

我没有看过 GCC 代码,所以我只是在做一些猜测和假设。

增量用于调整this指针。所以我们必须构造一个案例,其中:

MyClass* pThis = ...;
MemberFunctionPointer mfp = ...;
(pThis->*mfp)(); // must adjust this := pThis + delta

为什么 this(成员函数中的 this 指针)与 pThis 不同?如果我们从不同的 class:

调用成员函数 ,就会发生这种情况
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.

S 类型对象中的子对象 B2 偏移了 B1。因此,当我们写

S s;
s.memfun();

编译器必须有效地将地址从 &s 调整到 &s.d - 它应用了增量。


我们现在可以构造在成员函数指针中生成delta的例子:

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

这解释了为什么我们需要将增量存储为成员函数指针的一部分。


一个小陷阱是用于成员函数指针的 class 类型:

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,我们也可以通过 name/alias S::memfun.

找到它