Rokiのチラ裏

学生による学習のログ

vtableを使わず関数アドレスを解決させてvirtual functionを呼びだす

通常C++プログラマがvirtual functionを宣言する理由は、virtualメソッドへの関数ポインタが格納されているvtableを暗黙に作り出させ、動的なポリモーフィズムを実現させるためである。
しかし、vtable経由で動的に関数を間接的に呼び出すという事は、コンパイラが最適化を試みる事が難しくなるという事。そんな時に、明示的に指定する事によって、vtable経由ではない呼び出しが可能になる。結果的には、自前で最適化を試みる事と同義だが。

struct A{
    virtual const bool f()const{return false;}
    virtual ~A()=default;
};

struct Derived:A{
    const bool f()const override{return true;}
};

void f()
{
    A* obj=new Derived();
    assert(obj->f()); // vtable経由
    assert(static_cast<Derived*>(obj)->Derived::f()); // 静的な呼び出し
    delete obj;
}

このコードのvtableの様子。

/// ... 略
Vtable for A
A::_ZTV1A: 5u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::f
24    (int (*)(...))A::~A
32    (int (*)(...))A::~A

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x101b19360) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16u)

Vtable for Derived
Derived::_ZTV7Derived: 5u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Derived::f
24    (int (*)(...))Derived::~Derived
32    (int (*)(...))Derived::~Derived

Class Derived
   size=8 align=8
   base size=8 base align=8
Derived (0x0x142c0b208) 0 nearly-empty
    vptr=((& Derived::_ZTV7Derived) + 16u)
  A (0x0x101b19420) 0 nearly-empty
      primary-for Derived (0x0x142c0b208)

アセンブリ。前者が通常の呼び出し、後者がDerived*にキャストしながらスコープを明示的に指定したモノ。

0029 488B45E8       movq -24(%rbp), %rax
002d 488B00         movq (%rax), %rax
0030 488B00        movq (%rax), %rax
0033 488B55E8      movq -24(%rbp), %rdx
0037 4889D7        movq %rdx, %rdi
003a FFD0            call *%rax
003c 488B45E8        movq -24(%rbp), %rax
0040 4889C7        movq %rax, %rdi
0043 E8000000       call _ZNK7Derived1fEv

しかし、virtual functionを宣言する理由の一つであるポリモーフィズムは、明示的に指定している時点で、当然だが実現できていない。
vtable経由のオーバーヘッドが気になり、かつ型がわかりきっている時に限られる手法だと思う。

因みに文中のvtableの様子を出力させる方法は、gccであれば、-fdump-class-hierarchyオプションを指定する。

% cat test.cpp
struct A{
    virtual void f(){}
    virtual void g(){}
};

int main(){}
% g++ -fdump-class-hierarchy test.cpp
% cat test.cpp.002t.class            
Vtable for A
A::_ZTV1A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::f
24    (int (*)(...))A::g

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x142d626c0) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16u)

#追記

元々vtableを使わず、明示的にキャストするのであれば、そもそもオーバーライドさせる必要すらない。

struct X{
    void f()const{std::cout<<"X::"<<__func__<<std::endl;}
    virtual ~X()=default;
};

struct Y:X{
    void f()const{std::cout<<"Y::"<<__func__<<std::endl;}
};

struct Z:X{
    void f()const{std::cout<<"Z::"<<__func__<<std::endl;}
};

int main()
{
    auto af=[](auto ptr){
        ptr->f();
        return ptr;
    };

    delete af( new X() );
    delete af( static_cast<X*>(new Y()) );
    delete af( static_cast<X*>(new Z()) );
}
0010 488B45F0       movq -16(%rbp), %rax
0014 4889C7        movq %rax, %rdi
0017 E8000000       call _ZNK1X1fEv

vtableを作成していないのだから、アセンブリの出力結果は当然と言える。

参考

C++の仮想関数の仕組み&静的に呼ぶ方法: みやたけゆき劇場