Самый простой способ выяснить это - посмотреть фактическую реализацию.
Рассмотрим следующий код:
struct Base { virtual void foo() = 0; };
struct Derived { virtual void foo() { } };
Base& base();
void bar() {
Base& b = base();
b.foo(); // virtual call
}
А теперь, передайте это на страницу Try Out Clang, чтобы получить LLVM IR:
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
%struct.Base = type { i32 (...)** }
define void @_Z3barv() {
%1 = tail call %struct.Base* @_Z4basev()
%2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
%3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
tail call void %4(%struct.Base* %1)
ret void
}
declare %struct.Base* @_Z4basev()
Поскольку я полагаю, что вы еще не знаете об ИК, давайте рассмотрим его по частям.
Сначала придумайте вещи, о которых вам не следует беспокоиться. Он определяет архитектуру (процессор и систему), для которой он скомпилирован, а также его свойства.
; ModuleID = '/tmp/webcompile/_6336_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-unknown-linux-gnu"
Затем LLVM учат типам:
%struct.Base = type { i32 (...)** }
Анализирует типы структурно. Таким образом, здесь мы только получаем, что Base
будет состоять из одного элемента i32 (...)**
: это фактически «печально известный» указатель v-таблицы. Почему этот странный тип? Потому что мы будем хранить в v-таблице множество указателей на функции разных типов. Это означает, что у нас будет гетерогенный массив (что невозможно), поэтому вместо этого мы будем обращаться с ним как с массивом «общих» неизвестных элементов (чтобы отметить, что мы уверены в том, что там есть), и дело за приложением. указатель на соответствующий тип указателя на функцию перед его фактическим использованием (точнее, если бы мы были на C или C ++, IR намного ниже уровня).
Прыжки в конец:
declare %struct.Base* @_Z4basev()
при этом объявляется функция (_Z4basev
, название искажено), которая возвращает указатель на Base
: в IR ссылки и указатели оба представлены указателями.
Хорошо, давайте посмотрим определение bar
(или _Z3barv
в том виде, как оно искажено). Вот где лежат интересные вещи:
%1 = tail call %struct.Base* @_Z4basev()
Вызов base
, который возвращает указатель на Base
(тип возврата всегда точен на месте вызова, гораздо проще анализировать), он сохраняется в константе с именем %1
.
%2 = bitcast %struct.Base* %1 to void (%struct.Base*)***
Странный биткаст, который преобразует наш Base*
в указатель на странные вещи ... По сути, мы получаем указатель v-таблицы. Он не был «поименован», и мы просто убедились в определении типа, что это был первый элемент.
%3 = load void (%struct.Base*)*** %2, align 8
%4 = load void (%struct.Base*)** %3, align 8
Сначала мы загружаем v-таблицу (на которую указывает %2
), а затем загружаем указатель на функцию (на которую указывает %3
). На данный момент %4
, следовательно, &Derived::foo
.
tail call void %4(%struct.Base* %1)
Наконец, мы вызываем функцию и передаем ей элемент this
, явный здесь.