Как реализованы виртуальные функции и vtable?


110

Все мы знаем, что такое виртуальные функции в C ++, но как они реализованы на глубоком уровне?

Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?

Существует ли vtable для всех классов или только для тех, у которых есть хотя бы одна виртуальная функция?

Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?

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


2
Предлагаю прочитать шедевр Inside the C++ Object Modelавтора Stanley B. Lippman. (Раздел 4.2, страницы 124-131)
smwikipedia 05

Ответы:


123

Как виртуальные функции реализованы на глубоком уровне?

Из «Виртуальных функций в C ++» :

Каждый раз, когда в программе объявляется виртуальная функция, для класса создается av-table. V-таблица состоит из адресов виртуальных функций для классов, содержащих одну или несколько виртуальных функций. Объект класса, содержащего виртуальную функцию, содержит виртуальный указатель, указывающий на базовый адрес виртуальной таблицы в памяти. Всякий раз, когда есть вызов виртуальной функции, v-таблица используется для разрешения адреса функции. Объект класса, содержащий одну или несколько виртуальных функций, содержит виртуальный указатель, называемый vptr, в самом начале объекта в памяти. Следовательно, размер объекта в этом случае увеличивается на размер указателя. Этот vptr содержит базовый адрес виртуальной таблицы в памяти. Обратите внимание, что виртуальные таблицы зависят от класса, т. Е. существует только одна виртуальная таблица для класса независимо от количества виртуальных функций, которые он содержит. Эта виртуальная таблица, в свою очередь, содержит базовые адреса одной или нескольких виртуальных функций класса. В то время, когда для объекта вызывается виртуальная функция, vptr этого объекта предоставляет базовый адрес виртуальной таблицы для этого класса в памяти. Эта таблица используется для разрешения вызова функции, поскольку она содержит адреса всех виртуальных функций этого класса. Вот как разрешается динамическая привязка во время вызова виртуальной функции. vptr этого объекта предоставляет базовый адрес виртуальной таблицы для этого класса в памяти. Эта таблица используется для разрешения вызова функции, поскольку она содержит адреса всех виртуальных функций этого класса. Вот как разрешается динамическая привязка во время вызова виртуальной функции. vptr этого объекта предоставляет базовый адрес виртуальной таблицы для этого класса в памяти. Эта таблица используется для разрешения вызова функции, поскольку она содержит адреса всех виртуальных функций этого класса. Вот как разрешается динамическая привязка во время вызова виртуальной функции.

Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?

В целом, я считаю, что ответ - «нет». Вы можете немного изменить память, чтобы найти vtable, но вы все равно не знаете, как выглядит сигнатура функции для ее вызова. Все, что вы хотели бы достичь с помощью этой возможности (поддерживаемой языком), должно быть возможно без прямого доступа к vtable или ее изменения во время выполнения. Также обратите внимание, что спецификация языка C ++ не указывает, что требуются vtables, однако именно так большинство компиляторов реализуют виртуальные функции.

Существует ли vtable для всех объектов или только для тех, у которых есть хотя бы одна виртуальная функция?

Я считаю, что ответ здесь - «это зависит от реализации», так как спецификация вообще не требует vtables. Однако на практике я считаю, что все современные компиляторы создают виртуальную таблицу только в том случае, если в классе есть хотя бы 1 виртуальная функция. Существуют накладные расходы на пространство, связанные с vtable, и накладные расходы времени, связанные с вызовом виртуальной функции по сравнению с невиртуальной функцией.

Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?

Ответ в том, что он не определен спецификацией языка, поэтому это зависит от реализации. Вызов чистой виртуальной функции приводит к неопределенному поведению, если она не определена (что обычно не так) (ISO / IEC 14882: 2003 10.4-2). На практике он выделяет слот в vtable для функции, но не назначает ей адрес. Это оставляет vtable незавершенной, что требует, чтобы производные классы реализовали функцию и завершили vtable. Некоторые реализации просто помещают NULL-указатель в запись vtable; другие реализации помещают указатель на фиктивный метод, который делает что-то похожее на утверждение.

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

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

Это уже почти мои знания, так что, пожалуйста, помогите мне, если я ошибаюсь!

Я считаю, что только функции, которые являются виртуальными в классе, испытывают снижение производительности по времени, связанное с вызовом виртуальной функции по сравнению с невиртуальной функцией. В любом случае накладные расходы на пространство для класса есть. Обратите внимание: если есть виртуальная таблица, то ее будет только одна на класс , а не одна на объект .

Влияет ли на скорость, если виртуальная функция фактически переопределена или нет, или это не имеет никакого эффекта, пока она виртуальная?

Я не верю, что время выполнения переопределенной виртуальной функции уменьшается по сравнению с вызовом базовой виртуальной функции. Однако существует дополнительное пространство для класса, связанного с определением другой vtable для производного класса по сравнению с базовым классом.

Дополнительные ресурсы:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (через машину обратной связи)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable


2
Если бы компилятор поместил ненужный указатель vtable в объект, которому он не нужен, это не соответствовало бы философии C ++ Страуструпа. Правило состоит в том, что вы не получаете накладных расходов, которых нет в C, если вы этого не попросите, и компиляторам грубо нарушать это.
Стив Джессоп,

3
Я согласен с тем, что для любого компилятора, который серьезно относится к себе, было бы глупо использовать vtable при отсутствии виртуальных функций. Однако я счел важным указать на то, что, насколько мне известно, стандарт C ++ не требует / не требует / его, так что будьте осторожны, прежде чем зависеть от него.
Zach Burlingame

8
Даже виртуальные функции можно вызывать не виртуально. На самом деле это довольно часто: если объект находится в стеке, в пределах области видимости компилятор будет знать точный тип и оптимизирует поиск vtable. Это особенно верно для dtor, который должен вызываться в той же области стека.
MSalters

1
Я считаю, что когда у класса есть хотя бы одна виртуальная функция, каждый объект имеет vtable, а не одну для всего класса.
Asaf R,

3
Общая реализация: каждый объект имеет указатель на vtable; класс владеет столом. Магия построения просто состоит в обновлении указателя vtable в производном ctor после завершения работы базового ctor.
MSalters

31
  • Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?

Непереносимо, но если вы не против грязных трюков, конечно!

ВНИМАНИЕ : этот метод не рекомендуется использовать детям, взрослым в возрасте до 969 лет или маленьким пушистым существам из Альфы Центавра. Побочные эффекты могут включать в себя вылетающих из вашего носа демонов , внезапное появление Йог-Сотота в качестве необходимого утверждающего во всех последующих проверках кода или ретроактивное добавление IHuman::PlayPiano()ко всем существующим экземплярам]

В большинстве компиляторов, которые я видел, vtbl * - это первые 4 байта объекта, а содержимое vtbl представляет собой просто массив указателей на элементы (обычно в том порядке, в котором они были объявлены, с первым из базового класса). Конечно, есть и другие возможные варианты расположения, но я обычно наблюдал именно это.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

А теперь потянем за махинациями ...

Смена класса во время выполнения:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Замена метода для всех экземпляров (патч-обезьяна класса)

Это немного сложнее, поскольку сам vtbl, вероятно, находится в постоянной памяти.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

Последнее, скорее всего, заставит антивирусные программы и ссылку активировать и обратить внимание из-за манипуляций с mprotect. В процессе, использующем бит NX, он вполне может выйти из строя.


6
Хм. Кажется зловещим, что за это наградили. Надеюсь, это не значит, что @Mobilewits считает такие махинации на самом деле хорошей идеей ...
puetzk

1
Пожалуйста, подумайте о том, чтобы не поощрять использование этой техники, а не «подмигивать».
einpoklum

« Содержимое vtbl - это просто массив указателей на элементы» на самом деле это запись (структура) с разными записями, которые случайно расположены на
равных

1
Вы можете смотреть на это в любом случае; указатели функций имеют разные сигнатуры и, следовательно, разные типы указателей; в этом смысле он действительно похож на структуру. Но в других контекстах, но идея индекса vtbl полезна (например, ActiveX использует его так, как он описывает двойные интерфейсы в библиотеках типов), что является более похожим на массив представлением.
puetzk 02

17

Имеет ли одна виртуальная функция замедление работы всего класса?

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

Наличие виртуальных функций замедляет работу всего класса, поскольку необходимо инициализировать, скопировать еще один элемент данных ... при работе с объектом такого класса. Для класса с полдюжиной членов или около того разница должна быть незначительной. Для класса, который содержит только один charчлен или вообще не содержит членов, разница может быть заметной.

Кроме того, важно отметить, что не каждый вызов виртуальной функции является вызовом виртуальной функции. Если у вас есть объект известного типа, компилятор может выдать код для обычного вызова функции и даже может встроить указанную функцию, если сочтет нужным. Только когда вы выполняете полиморфные вызовы через указатель или ссылку, которые могут указывать на объект базового класса или на объект какого-либо производного класса, вам понадобится косвенное обращение к vtable и заплатите за это с точки зрения производительности.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

Действия, которые должно предпринять оборудование, по существу одинаковы, независимо от того, перезаписана функция или нет. Адрес vtable считывается из объекта, указатель функции извлекается из соответствующего слота, а функция вызывается указателем. С точки зрения реальной производительности предсказания ветвлений могут иметь некоторое влияние. Так, например, если большинство ваших объектов относятся к одной и той же реализации данной виртуальной функции, то есть некоторая вероятность того, что предсказатель ветвления правильно предсказывает, какую функцию вызывать, даже до того, как указатель был получен. Но не имеет значения, какая функция является общей: это может быть большинство объектов, делегирующих неперезаписываемый базовый вариант, или большинство объектов, принадлежащих одному подклассу и, следовательно, делегируемых одному и тому же перезаписанному случаю.

как они реализованы на глубоком уровне?

Мне нравится идея jheriko продемонстрировать это с помощью фиктивной реализации. Но я бы использовал C для реализации чего-то похожего на приведенный выше код, чтобы было легче увидеть низкий уровень.

родительский класс Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

производный класс Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

функция f, выполняющая вызов виртуальной функции

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Итак, вы можете видеть, что vtable - это просто статический блок в памяти, в основном содержащий указатели на функции. Каждый объект полиморфного класса будет указывать на vtable, соответствующую его динамическому типу. Это также делает связь между RTTI и виртуальными функциями более ясной: вы можете проверить, к какому типу относится класс, просто посмотрев, на какую vtable он указывает. Вышеизложенное упрощено во многих отношениях, например, с множественным наследованием, но общая концепция разумна.

Если argимеет тип Foo*и вы берете arg->vtable, но на самом деле это объект типа Bar, то вы все равно получите правильный адрес vtable. Это потому, что vtableвсегда является первым элементом по адресу объекта, независимо от того, вызывается он vtableили base.vtableв правильно набранном выражении.


«Каждый объект полиморфного класса будет указывать на свою собственную виртуальную таблицу». Вы говорите, что у каждого объекта есть своя собственная таблица? AFAIK vtable используется всеми объектами одного класса. Дай мне знать, если я ошибаюсь.
Bhuwan

1
@Bhuwan: Нет, вы правы: существует только одна виртуальная таблица для каждого типа (что может быть для каждого экземпляра шаблона в случае шаблонов). Я хотел сказать, что каждый объект полиморфного класса указывает на применимую к нему виртуальную таблицу, поэтому каждый объект имеет такой указатель, но для объектов того же типа он будет указывать на одну и ту же таблицу. Наверное, стоит перефразировать это.
MvG

1
@MvG « объекты того же типа, который он будет указывать на ту же таблицу », а не во время построения базовых классов с виртуальными базовыми классами! (очень особый случай)
curiousguy

1
@curiousguy: Я бы напишу это в разделе «все вышеизложенное упрощено во многих отношениях», особенно потому, что основное применение виртуальных баз - множественное наследование, которое я тоже не моделировал. Но спасибо за комментарий, полезно иметь это здесь для людей, которым может потребоваться больше информации.
MvG


2

Этот ответ был включен в ответ сообщества Wiki

  • Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?

Ответ на этот вопрос заключается в том, что она не указана - вызов чистой виртуальной функции приводит к неопределенному поведению, если она не определена (что обычно не так) (ISO / IEC 14882: 2003 10.4-2). Некоторые реализации просто помещают NULL-указатель в запись vtable; другие реализации помещают указатель на фиктивный метод, который делает что-то похожее на утверждение.

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


Кроме того, я не думаю, что абстрактный класс может определять реализацию чистой виртуальной функции. По определению, чистая виртуальная функция не имеет тела (например, bool my_func () = 0;). Однако вы можете предоставить реализации для обычных виртуальных функций.
Zach Burlingame

Чистая виртуальная функция может иметь определение. См. Статью № 34 Скотта Мейерса «Эффективный C ++, 3-е издание», ISO 14882-2003 10.4-2, или bytes.com/forum/thread572745.html
Майкл Берр,

2

Вы можете воссоздать функциональность виртуальных функций в C ++, используя указатели функций как члены класса и статические функции как реализации, или используя указатель на функции-члены и функции-члены для реализаций. Между этими двумя методами есть только преимущества в обозначениях ... на самом деле вызовы виртуальных функций сами по себе являются просто удобством записи. На самом деле наследование - это просто удобство записи ... все это может быть реализовано без использования языковых функций для наследования. :)

Приведенный ниже дерьмовый непроверенный код, вероятно, содержит ошибки, но, надеюсь, демонстрирует идею.

например

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

void(*)(Foo*) MyFunc;это какой-то синтаксис Java?
curiousguy

нет, это синтаксис C / C ++ для указателей на функции. Процитирую себя: «Вы можете воссоздать функциональность виртуальных функций в C ++, используя указатели на функции». это неприятный синтаксис, но то, что нужно знать, если вы считаете себя программистом на C.
jheriko

Указатель на функцию ac будет иметь вид: int ( PROC) (); и указатель на функцию-член класса будет выглядеть так: int (ClassName :: MPROC ) ();
Menace

1
@menace, вы забыли какой-то синтаксис ... может быть, вы думаете о typedef? typedef int (* PROC) (); так что вы можете просто выполнить PROC foo позже вместо int (* foo) ()?
jheriko

2

Постараюсь сделать проще :)

Все мы знаем, что такое виртуальные функции в C ++, но как они реализованы на глубоком уровне?

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

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

  • Производный класс не добавляет новые виртуальные функции и не отменяет их. В этом случае этот класс разделяет виртуальную таблицу с базовым классом.
  • Производный класс добавляет и переопределяет виртуальные методы. В этом случае он получает свою собственную vtable, где добавленные виртуальные функции имеют индекс, начинающийся после последней производной.
  • Множественные полиморфные классы в наследовании. В этом случае у нас есть сдвиг индекса между второй и следующей базами и его индекс в производном классе

Можно ли изменить vtable или даже получить к ней прямой доступ во время выполнения?

Нестандартный способ - нет API для доступа к ним. У компиляторов могут быть некоторые расширения или частные API для доступа к ним, но это может быть только расширение.

Существует ли vtable для всех классов или только для тех, у которых есть хотя бы одна виртуальная функция?

Только те, которые имеют хотя бы одну виртуальную функцию (будь то даже деструктор) или производят хотя бы один класс, имеющий свою vtable («полиморфен»).

Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?

Это возможная реализация, но не практикуется. Вместо этого обычно есть функция, которая выводит что-то вроде «вызвана чистая виртуальная функция» и выполняет abort(). Вызов этого может произойти, если вы попытаетесь вызвать абстрактный метод в конструкторе или деструкторе.

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

Замедление зависит только от того, разрешен ли вызов как прямой вызов или как виртуальный вызов. И все остальное не имеет значения. :)

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

  • Если вы вызываете метод через значение (переменную или результат функции, которая возвращает значение) - в этом случае компилятор не сомневается, каков фактический класс объекта, и может «жестко разрешить» его во время компиляции. .
  • Если виртуальный метод объявлен finalв классе, на который у вас есть указатель или ссылка, через которую вы его вызываете ( только в C ++ 11 ). В этом случае компилятор знает, что этот метод не может подвергаться дальнейшему переопределению и может быть только методом из этого класса.

Обратите внимание, что виртуальные вызовы имеют только накладные расходы на разыменование двух указателей. Использование RTTI (хотя и доступно только для полиморфных классов) медленнее, чем вызов виртуальных методов, если вам удастся реализовать одно и то же двумя способами. Например, определение, virtual bool HasHoof() { return false; }а затем переопределение только для того, bool Horse::HasHoof() { return true; }чтобы предоставить вам возможность вызывать, if (anim->HasHoof())который будет быстрее, чем попытки if(dynamic_cast<Horse*>(anim)). Это связано с тем, что dynamic_castв некоторых случаях приходится проходить по иерархии классов даже рекурсивно, чтобы увидеть, можно ли построить путь из фактического типа указателя и желаемого типа класса. В то время как виртуальный вызов всегда один и тот же - разыменование двух указателей.


2

Вот работающая ручная реализация виртуальной таблицы на современном C ++. У него четко определенная семантика, никаких хаков и нет void*.

Примечание: .*и ->*разные операторы , чем *и ->. Указатели на функции-члены работают иначе.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

1

У каждого объекта есть указатель vtable, который указывает на массив функций-членов.


1

Во всех этих ответах здесь не упоминается то, что в случае множественного наследования, когда все базовые классы имеют виртуальные методы. У наследующего класса есть несколько указателей на vmt. В результате размер каждого экземпляра такого объекта больше. Всем известно, что класс с виртуальными методами имеет 4 дополнительных байта для vmt, но в случае множественного наследования для каждого базового класса виртуальные методы умножены на 4. 4 - это размер указателя.


0

Ответы Берли здесь верны, за исключением вопроса:

Имеют ли абстрактные классы просто NULL для указателя функции хотя бы одной записи?

Ответ заключается в том, что для абстрактных классов не создается вообще никакой виртуальной таблицы. В этом нет необходимости, поскольку нельзя создавать объекты этих классов!

Другими словами, если у нас есть:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

Указатель vtbl, доступ к которому осуществляется через pB, будет vtbl класса D. Именно так и реализован полиморфизм. То есть, как доступ к методам D осуществляется через pB. Для класса B. нет необходимости в vtbl.

В ответ на комментарий Майка ниже ...

Если у класса B в моем описании есть виртуальный метод foo (), который не переопределяется D, и виртуальный метод bar (), который переопределяется, тогда vtbl D будет иметь указатель на foo () B и на его собственный bar () . Для B. все еще не создан vtbl.


Это неверно по двум причинам: 1) абстрактный класс может иметь обычные виртуальные методы в дополнение к чистым виртуальным методам, и 2) чистые виртуальные методы могут необязательно иметь определение, которое может быть вызвано с полностью определенным именем.
Майкл Берр,

Правильно - если подумать, я предполагаю, что если бы все виртуальные методы были чисто виртуальными, компилятор мог бы оптимизировать vtable (ему потребуется помощь в формировании компоновщика, чтобы убедиться, что также не было определений).
Майкл Берр,

1
« Ответ состоит в том, что для абстрактных классов не создается вообще никакой виртуальной таблицы ». Неправильно. « В этом нет необходимости, поскольку объекты этих классов не могут быть созданы! » Неправильно.
curiousguy

Я могу следовать вашему обоснованию , что ни на виртуальных таблицах B не должно быть необходимо. Тот факт, что некоторые из его методов имеют (по умолчанию) реализации, не означает, что они должны храниться в vtable. Но я просто прогнал ваш код (по модулю некоторых исправлений, чтобы он скомпилировался), gcc -Sа затем, c++filtочевидно, есть vtable для Bвключения туда. Я предполагаю, что это может быть потому, что vtable также хранит данные RTTI, такие как имена классов и наследование. Это может потребоваться для dynamic_cast<B*>. Даже -fno-rttiне убирает vtable. С clang -O3вместо gccэто вдруг исчезло.
MvG

@MvG " То, что некоторые из его методов имеют (по умолчанию) реализации, не означает, что они должны храниться в vtable " Да, это означает именно это.
curiousguy

0

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

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

вывод:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

обратите внимание, поскольку я никогда не выделяю свой фальшивый объект, нет необходимости производить какое-либо уничтожение; деструкторы автоматически помещаются в конец области динамически выделяемых объектов, чтобы освободить память самого литерала объекта и указателя vtable.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.