Почему fdump-class-hierarchy дает два указателя int vtable для виртуальных функций

для следующего класса,

class A
{
public:

    char VarA;
    int VarB;

    virtual ~A(){}
};

g++ fdump-class-hierarchy дает мне Vtable,

Vtable for A
A::_ZTV1A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::~A
24    (int (*)(...))A::~A

но я не понимаю: 1. Какие два первых указателя? 2. Почему 2 указателя на виртуальный деструктор?

Спасибо!


person Juan Camilo Acosta Arango    schedule 06.05.2018    source источник
comment
Может быть интересно: refspecs.linuxbase.org/cxxabi-1.83.html#vtable. В частности, смещение вверх, RTTI, пара деструкторов.   -  person Kerrek SB    schedule 06.05.2018


Ответы (1)


Давайте попробуем со следующим демонстрационным кодом:

class A
{
public:

    char VarA;
    int VarB;

    virtual ~A();
};

#include <cstdio>

A::~A() { 
    std::printf("A::~A()\n");
}

#include <typeinfo>

void *complete_object_addr(A &ref) {
    return dynamic_cast<void*> (&ref);
}

const std::type_info& get_typeinfo(A &ref) {
    return typeid(ref);
}

void explicit_destructor_call(A *p) {
    p->~A();
}

void delete_object(A *p) {
    delete p;
}

#include <memory>

void create_object (A *p) {
    new (p) A;
}

в Godbolt.

Виртуальная таблица

Начнем с вызова конструктора

void create_object (A *p) {
    new (p) A;
}

чтобы увидеть, где находится vptr и vtable:

create_object(A*):
        movq    $vtable for A+16, (%rdi)
        ret

Параметр A* находится в rdi, vptr имеет смещение 0 в объекте, поэтому *(void*)p дает vptr. Виртуальная таблица создается как:

vtable for A:
        .quad   0
        .quad   typeinfo for A
        .quad   A::~A() [complete object destructor]
        .quad   A::~A() [deleting destructor]

Мы видим, что vptr указывает внутрь (не в начало vtable): .quad означает 8 байт до vptr указывает на третий элемент: A::~A() [complete object destructor].

Этот способ вывода vtable намного понятнее, чем в вашем вопросе: есть два деструктора, которые можно вызвать виртуально:

  • "[полный деструктор объекта]" для уничтожения всего объекта,
  • «[удаление деструктора]» для удаления всего объекта.

Виртуальные деструкторы

Действительно, код для explicit_destructor_call(A*) определяется как

void explicit_destructor_call(A *p) {
    p->~A();
}

показывает вызов указателя функции vptr[0].

explicit_destructor_call(A*):
        movq    (%rdi), %rax
        jmp     *(%rax)

Вызов виртуальной функции — (p->vptr)(p); обратите внимание, что передача аргумента this подразумевается в сгенерированном коде, поскольку он находится в регистре.

Здесь есть хитрость, и вам нужно будет отключить фильтр директив сборки, чтобы увидеть ее:

.text
.size   A::~A() [base object destructor], .-A::~A() [base object destructor]
.globl  A::~A() [complete object destructor]
.set    A::~A() [complete object destructor],A::~A() [base object destructor]

Я не привык к этим директивам, но это, безусловно, означает, что A::~A() [complete object destructor] на самом деле A::~A() [base object destructor], то есть:

.LC0:
        .string "A::~A()"
A::~A() [base object destructor]:
        movq    $vtable for A+16, (%rdi)
        movl    $.LC0, %edi
        jmp     puts
  • сначала vptr устанавливается как в конструкторе: динамический тип *this сбрасывается из A, что полезно только для уничтожения подобъектов базового класса, а не полных объектов,
  • затем вызывается puts("A::~A()"); (что показывает, что строка спецификации printf по возможности интерпретируется во время компиляции).

Функция delete_object(A*) определена как

void delete_object(A *p) {
    delete p;
}

компилируется как

delete_object(A*):
        testq   %rdi, %rdi
        je      .L8
        movq    (%rdi), %rax
        jmp     *8(%rax)

Это немного сложнее: требуется проверка p!=0, поскольку delete p; допустимо, когда p равно нулю. Если p не равно нулю, код переходит к (char*)vptr + 8, который является следующим элементом в vtable: vptr[1].

Этот деструктор скомпилирован как:

A::~A() [deleting destructor]:
        pushq   %rbx
        movq    %rdi, %rbx
        movq    $vtable for A+16, (%rdi)
        movl    $.LC0, %edi
        call    puts
        movq    %rbx, %rdi
        movl    $16, %esi
        popq    %rbx
        jmp     operator delete(void*, unsigned long)

Сначала мы сбрасываем динамический тип на A. (Я не думаю, что это действительно когда-либо понадобится.) Затем мы выводим текст, как в деструкторе «просто уничтожить объект», затем мы вызываем operator delete(this,sizeof(A));.

RTTI: typeid, dynamic_cast

Получить значение typeid(lvalue of A) очень просто;

const std::type_info& get_typeinfo(A &ref) {
    return typeid(ref);
}

компилируется как

get_typeinfo(A&):
        movq    (%rdi), %rax
        movq    -8(%rax), %rax
        ret

Мы видим, что vptr[-1] читается и возвращается.

И, наконец, получение адреса самого производного объекта по dynamic_cast<void*>

void *complete_object_addr(A &ref) {
    return dynamic_cast<void*> (&ref);
}

предельно просто:

complete_object_addr(A&):
        movq    (%rdi), %rax
        addq    -16(%rax), %rdi
        movq    %rdi, %rax
        ret

Самый производный объект, который находится по смещению vptr[-2] после this (эти смещения почти всегда отрицательные).

person curiousguy    schedule 01.07.2018