Если классы с виртуальными функциями реализуются с помощью vtables, то как реализуется класс без виртуальных функций?


В частности, не должен ли быть какой-то указатель функции на месте в любом случае?

9 6

9 ответов:

Не виртуальные функции-члены на самом деле являются просто синтаксическим сахаром, поскольку они почти как обычная функция, но с проверкой доступа и неявным параметром объекта.

struct A 
{
  void foo ();
  void bar () const;
};

В основном то же самое, что:

struct A 
{
};

void foo (A * this);
void bar (A const * this);

Vtable нужна для того, чтобы мы вызывали правильную функцию для нашего конкретного экземпляра объекта. Например, если мы имеем:

struct A 
{
  virtual void foo ();
};

Реализация 'foo' может приближаться к чему-то вроде:

void foo (A * this) {
  void (*realFoo)(A *) = lookupVtable (this->vtable, "foo");
  (realFoo)(this);   // Make the call to the most derived version of 'foo'
}

Я думаю, что фраза "классы с виртуальными функциями реализуются с помощью vtables " вводит вас в заблуждение.

Эта фраза звучит так, как будто классы с виртуальными функциями реализуются " способом A ", а классы без виртуальных функций реализуются " способом B".

В реальности классы с виртуальными функциями, в дополнение к будучи реализованными как классы, они также имеют vtable. Другой способ увидеть это заключается в том, что "'vtables' реализуют 'виртуальную функцию ' часть класса".

Подробнее о том, как они оба работают:

Все классы (с виртуальными или невиртуальными методами) являются структурами. Единственное различие между структурой и классом в C++ состоит в том, что по умолчанию члены являются открытыми в структурах и частными в классах. Поэтому я буду использовать термин class здесь для обозначения как структур, так и классов. Помните, что это почти синонимы!

Члены Данных

Классы являются (как и структуры) просто блоки непрерывной памяти, где каждый член хранится в последовательности. Обратите внимание, что в некоторых случаях по архитектурным причинам между элементами процессора будут возникать пробелы, поэтому блок может быть больше, чем сумма его частей.

Методы

Методы или "функции-члены" - это иллюзия. В действительности такой вещи, как "функция-член", не существует. Функция-это всегда просто последовательность инструкций машинного кода, хранящихся где-то в памяти. Чтобы сделать звонок, процессор переходит в эту позицию памяти и начинает выполнение. Можно сказать, что все методы и функции являются "глобальными", и любое указание на обратное является удобной иллюзией, навязанной компилятором. Очевидно, что метод действует так, как будто он принадлежит определенному объекту, поэтому очевидно, что происходит нечто большее. Чтобы привязать конкретный вызов метода (функции) к конкретному объекту, каждый метод-член имеет скрытый аргумент, который является указателем на рассматриваемый объект. Член скрыт в том, что вы не добавляете его в свой код C++ сами, но в этом нет ничего волшебного-это очень реально. Когда вы говорите это:
void CMyThingy::DoSomething(int arg);
{
    // do something
}

Компилятор действительно делает это:

void CMyThingy_DoSomething(CMyThingy* this, int arg)
{
    /do something
}

Наконец, когда вы пишете это:

myObj.doSomething(aValue);

Компилятор говорит:

CMyThingy_DoSomething(&myObj, aValue);

Нет необходимости в указателях функций в любом месте! Компилятор уже знает, какой метод вы вызываете, поэтому он вызывает его напрямую.

Статические методы еще проще. У них нет указателя this , поэтому они реализуются именно так, как вы их пишете.

Вот так! Остальное-просто удобный синтаксический шугаринг: компилятор знает, к какому классу принадлежит метод, поэтому он не позволяет вам вызывать функцию, не указав, к какому именно. Он также использует это знание для перевода myItem в this->myItem, когда это однозначно.

(Да, это верно: доступ к элементам в методе Всегда осуществляется косвенно через указатель, даже если вы не видите один)

(Edit : удалено последнее предложение и опубликовано отдельно, чтобы его можно было критиковать отдельно)

Виртуальные методы требуются, когда вы хотите использовать полиморфизм. Модификатор virtual помещает метод в VMT для позднего связывания, а затем во время выполнения решается, какой метод из какого класса выполняется.

Если метод не является виртуальным-во время компиляции решается, из какого экземпляра класса он будет выполняться.

Указатели функций используются в основном для обратных вызовов.

Если класс с виртуальной функцией реализуется с помощью vtable, то класс без виртуальной функции реализуется без vtable.

Vtable содержит указатели функций, необходимые для отправки вызова соответствующему методу. Если метод не является виртуальным, вызов переходит к известному типу класса, и не требуется никаких косвенных действий.

Для невиртуального метода компилятор может генерировать обычный вызов функции (например, вызов определенного адреса с этим указателем, переданным в качестве параметра) или даже встроить его. Для виртуальной функции компилятор обычно не знает во время компиляции, по какому адресу вызвать код, поэтому он генерирует код, который ищет адрес в vtable во время выполнения, а затем вызывает метод. Правда, даже для виртуальных функций компилятор иногда может корректно решить правильный код на время компиляции (например, методы на локальных переменных, вызываемых без указателя / ссылки).

(я вытащил этот раздел из своего первоначального ответа, чтобы его можно было критиковать отдельно. Это намного более лаконично и по существу вашего вопроса, так что в некотором смысле это гораздо лучший ответ)

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

Компилятор вызывает глобальную функцию с указателем на объект вместо вызова некоторой указательной функции внутри объекта

Почему? Потому что это, как правило, гораздо более эффективным способом. Косвенные звонки-это дорогостоящие инструкции.

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

Ветви генерируются непосредственно к скомпилированному коду для методов; так же, как если у вас есть функции, которые вообще не находятся в классе, ветви генерируются прямо к ним.

Компилятор / компоновщик напрямую связывает, какие методы будут вызваны. Нет необходимости в косвенном указании vtable. Кстати, какое это имеет отношение к "стеку против кучи"?