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