поиск в виртуальной таблице c ++ - как она выполняет поиск и замену - PullRequest
6 голосов
/ 04 ноября 2011

Давайте рассмотрим пример ниже:

class Base{
    virtual string function1(){ return "Base - function1"; };
    virtual string function2(){ return "Base - function2"; };
};

class Derived : public Base {
    virtual string function2(){ return "Derived - function2"; };
    virtual string function1(){ return "Derived - function1"; };
    string function3() { return "Derived - function3"; };
};

Таким образом, структура vtable выглядит как

Base-vTable
-----------------------
name_of_function address_of_function
function1   &function1
function2   &function2
-----------------------
-----------------------
Derived-vTable
-----------------------
name_of_function address_of_function
function1   &function1
function2   &function2

или как

    Base-vTable
-----------------------
    Offset function
    +0  function1
    +4  function2
-----------------------
-----------------------
    Derived-vTable
-----------------------
    Offset function
    +0  function1
    +4  function2

Если она похожа на последнюю?тогда что это за смещение?где это используется?

И имя функции: это искаженное имя функции?если он искажен, то базовые и производные искаженные имена не будут совпадать, и поиск в vtable не будет работать.Компилятор обрабатывает все имена виртуальных функций, так что это должно быть искаженное имя, означает ли это, что искаженное имя для base & output одинаково в случае, если это виртуальная функция.

Ответы [ 6 ]

8 голосов
/ 04 ноября 2011

Виртуальные таблицы - это просто массивы указателей функций, как и ваш второй фрагмент. Компилятор переводит вызовы виртуальных функций в вызовы через указатель, например

Base * b = /* ... */;
b->function2();

переводится на

b->__vtable[1]();

где я использовал имя __vtable для ссылки на виртуальную таблицу (однако учтите, что виртуальная таблица обычно не доступна напрямую).

Порядок записей в таблице определяется порядком, в котором функции объявлены в классе. Помните, что определение класса всегда доступно в точке вызова.

3 голосов
/ 04 ноября 2011

Самый простой способ выяснить это - посмотреть фактическую реализацию.

Рассмотрим следующий код:

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, явный здесь.

3 голосов
/ 04 ноября 2011

Я объясняю следующий код. Я думаю, это прояснит вам

  Base *p = new Derived;
  p->function2();

Во время компиляции создается VTable, VTable класса Base идентичен VTable класса Derived ,. Я имею в виду, что оба имеют 2 функции, как вы упомянули в первом случае. Компилятор вставляет код для инициализации vptr нужного объекта.

Когда компилятор видит оператор p-> function2 () ;, он не привязывается к вызываемой функции, поскольку t знает только о базовом объекте. Из VTable класса Base выясняется позиция функции 2 (вот вторая позиция в VTable).

Во время выполнения VTable класса Dervied назначается vptr. Функция во втором положении VTable вызывается.

2 голосов
/ 04 ноября 2011

Второй случай - исходящие указатели занимают 4 байта (32-битные машины).

Имена функций никогда не сохраняются в исполняемом файле (за исключением отладки). Виртуальная таблица - это просто вектор указателей на функции, к которым напрямую обращается работающий код.

1 голос
/ 04 ноября 2011

Когда в класс добавляется виртуальная функция, компилятор создает скрытый указатель (называемый v-ptr) в качестве члена класса. [Вы можете проверить это, взяв sizeof (class), который увеличивается на sizeof (указатель)] Также компилятор внутренне добавляет некоторый код в начале конструктора, чтобы инициализировать v-ptr к базовому смещению v-таблицы класса. Теперь, когда этот класс наследуется другим классом, тогда этот v-ptr также наследуется классом Derived. А для производного класса этот v-ptr инициализируется с базовым смещением v-таблицы производного класса. И мы уже знаем, что v-таблицы соответствующих классов будут хранить адреса своих версий виртуальных функций. [Обратите внимание, что если виртуальная функция не переопределена в производном классе, то адрес v-версии или самой производной-версии (для многоуровневого наследования) функции в иерархии будет сохранен в v-таблице]. Следовательно, во время выполнения он просто вызывает функцию через этот v-ptr. Таким образом, если указатель базового класса хранит базовый объект, тогда вступает в действие базовая версия v-ptr. Так как он указывает на базовую версию v-таблицы, автоматически будет вызываться базовая версия функции. То же самое относится и к производному объекту.

1 голос
/ 04 ноября 2011

Это на самом деле зависит от компилятора, стандарт не определяет, как работает представление памяти.Стандарт гласит, что полиморфизм должен всегда работать (даже в случае inline функций, как у вас).Ваши функции могут быть встроены, в зависимости от контекста и умения компилятора, поэтому иногда call или jmp могут даже не возникать.Однако на большинстве компиляторов наиболее вероятен второй вариант.

Для вашего случая:

class Base{
    virtual string function1(){ return "Base - function1"; };
    virtual string function2(){ return "Base - function2"; };
};

class Derived : public Base {
    virtual string function2(){ return "Derived - function2"; };
    virtual string function1(){ return "Derived - function1"; };
};

Предположим, у вас есть:

Base* base = new Base;
Base* derived = new Derived;

base->function1();
derived->function2();

Для первоговызов, компилятор получит адрес vftable для Base и вызовет первую функцию, в этом vftable.Для второго вызова vftable находится в другом месте, поскольку объект на самом деле имеет тип Derived.Он ищет вторую функцию, переходя к смещению от начала vftable, в котором встречаются функции (то есть vftable + offset - скорее всего 4 байта, но опять же, зависит от платформы).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...