Почему fdump-class -ierarchy дает два указателя int vtable для виртуальных функций - PullRequest
0 голосов
/ 06 мая 2018

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

class A
{
public:

    char VarA;
    int VarB;

    virtual ~A(){}
};

g ++ fdump-class-иерархия дает мне для 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 указателя для виртуального деструктора?

Спасибо!

1 Ответ

0 голосов
/ 01 июля 2018

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

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;
}

в Годболт .

Vtable

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

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 генерируется как:

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, который является следующим элементом в таблице: 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 (эти смещения почти всегда отрицательны).

...