C++のメンバー関数ポインタが16バイトなのはなぜですか?

8768 ワード

ポインタについて議論するとき、通常はvoid *ポインタで表すことができるものと仮定し、x 86_64プラットフォームの下には8バイトのサイズがあります.例えば、以下はウィキペディアからx 86についてです.64の記事の抜粋:
Pushes and pops on the stack are always in 8-byte strides, and pointers are 8 bytes wide.
CPUの観点から見ると、ポインタはメモリのアドレスにほかならず、すべてのメモリアドレスはx 86_である.64プラットフォームの下はすべて64ビットで表されるので、8バイトが正しいと仮定します.異なるタイプのポインタの長さを簡単に出力することで、私たちが言っていることを検証するのも難しくありません.
#include <iostream> int main() { std::cout << "sizeof(int*) == " << sizeof(int*) << "
" "sizeof(double*) == " << sizeof(double*) << "
" "sizeof(void(*)()) == " << sizeof(void(*)()) << std::endl; }

コンパイルは、上記のプログラムを実行し、結果からすべてのポインタの長さが8バイトであることがわかります.
$ uname -i
x86_64
$ g++ -Wall ./example.cc
$ ./a.out
sizeof(int*)      == 8
sizeof(double*) == 8 sizeof(void(*)()) == 8

しかし、C++には、メンバー関数のポインタという特例があります.興味深いでしょう.メンバー関数ポインタは他の任意のポインタの長さの2倍です.これは、次の簡単なプログラムで検証できます.出力の結果は「16」です.
#include <iostream> struct Foo { void bar() const { } }; int main() { std::cout << sizeof(&Foo::bar) << std::endl; }

これはウィキペディアで間違っていると思っていますか?明らかに違う!ハードウェアの観点から、すべてのポインタは依然として8バイトです.それなら、メンバー関数のポインタは何ですか.これはC++言語の特性です.ここでメンバー関数のポインタはハードウェアに直接マッピングされていません.実行時(コンパイラ)によって実装されると、いくつかの追加のオーバーヘッドが発生し、通常はパフォーマンスの損失が発生します.C++言語仕様では実装の詳細は記載されておらず、このタイプのポインタも説明されていません.幸いなことに、Itanium C++ABI仕様では、C++の実行時に実装される詳細が共有されています.たとえば、Virtual Table、RTTI、例外がどのように実装されているかを説明しています.§2.3では、メンバーポインタも説明されています.
A pointer to member function is a pair as follows:
ptr:
For a non-virtual function, this field is a simple function pointer. For a virtual function, it is 1 plus the virtual table offset (in bytes) of the function, represented as a ptrdiff_t. The value zero represents a NULL pointer, independent of the adjustment field value below.
adj:
The required adjustment to this, represented as a ptrdiff_t.
したがって、メンバーポインタは8バイトではなく16バイトであり、単純な関数ポインタの後に「this」ポインタをどのように調整するか(常に非静的メンバー関数に暗黙的に渡される)の情報を保存する必要があるためです.ABI仕様では、なぜこのポインタを調整する必要があるのか、いつ調整する必要があるのかは説明されていません.最初は明らかではないかもしれませんが、次のクラス継承の例を見てみましょう.
struct A { void foo() const { } char pad0[32]; }; struct B { void bar() const { } char pad2[64]; }; struct C : A, B { };

AとBには、いずれも非静的メンバー関数とデータメンバーがあります.この2つの方法は、クラス内のデータ・メンバーに「this」ポインタを暗黙的に渡すことによってアクセスできます.任意のデータ・メンバーにアクセスするには、「this」「ポインタにオフセットを付けると、オフセットはクラスオブジェクトのベースアドレスへのデータメンバーのオフセットです.ptrdiff_tで表すことができます.しかし、多重継承時には複雑になります.AとBを継承したクラスCがありますが、何が起こるのでしょうか.コンパイラはAとBを同時にメモリに入れます.BはAの下にあります.そのため、Aクラスの方法とBクラスの方法で見られるthisポインタの値は違います.これは、次のように簡単に検証できます.
#include <iostream>

struct A {
    void foo() const {
        std::cout << "A's this: " << this << std::endl;
    }
    char pad0[32];
};

struct B {
    void bar() const {
        std::cout << "B's this: " << this << std::endl;
    }
    char pad2[64];
};

struct C : A, B
{ };

int main()
{
    C obj;
    obj.foo();
    obj.bar();
}
$ g++ -Wall -o test ./test.cc && ./test A's this: 0x7fff57ddfb48 B's this: 0x7fff57ddfb68

ご覧のように、thisポインタの値がBに渡される方法は、Aの方法よりも32バイト大きい--クラスAオブジェクトの実際のサイズです.しかし,次の関数をポインタでクラスCのメソッドを呼び出すと,何が起こるのだろうか.
void call_by_ptr(const C &obj, void (C::*mem_func)() const) { (obj.*mem_func)(); }

どの関数を呼び出すかに関係して、異なるthisポインタ値がこれらの関数に渡されます.しかしcall_by_ptr関数はそのパラメータがfoo()のポインタなのかbar()のポインタなのか分からず,この情報を知る唯一のタイミングはこれらの方法の使用時である.これは、メンバー関数のポインタが呼び出される前にthisポインタを調整する方法を知る必要がある理由です.ここでは、すべてのプログラムを簡単なプログラムに配置し、内部作業のメカニズムを説明します.
#include <iostream> struct A { void foo() const { std::cout << "A's this:\t" << this << std::endl; } char pad0[32]; }; struct B { void bar() const { std::cout << "B's this:\t" << this << std::endl; } char pad2[64]; }; struct C : A, B { }; void call_by_ptr(const C &obj, void (C::*mem_func)() const) { void *data[2]; std::memcpy(data, &mem_func, sizeof(mem_func)); std::cout << "------------------------------
" "Object ptr:\t" << &obj << "
Function ptr:\t" << data[0] << "
Pointer adj:\t" << data[1] << std::endl; (obj.*mem_func)(); } int main() { C obj; call_by_ptr(obj, &C::foo); call_by_ptr(obj, &C::bar); }

上記のプログラムの出力は次のとおりです.
------------------------------
Object ptr:    0x7fff535dfb28
Function ptr:  0x10c620cac
Pointer adj:   0
A's this:    0x7fff535dfb28
------------------------------
Object ptr:    0x7fff535dfb28
Function ptr:  0x10c620cfe
Pointer adj:   0x20
B's this:    0x7fff535dfb48

本文が問題をより明確にすることを望んでいる.
 
翻訳:http://741mhz.com/wide-pointers/