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

Мы все знаем, какие виртуальные функции находятся на С++, но как они реализованы на глубоком уровне?

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

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

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

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

Ответ 1

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

От "Виртуальные функции на С++"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 или даже получить прямой доступ во время выполнения?

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

ПРЕДУПРЕЖДЕНИЕ. Этот метод не рекомендуется использовать детям, взрослым в возрасте 969, или небольшим пушистые существа из Альфы Центавра. Побочные эффекты могут включать демоны, которые вылетают из вашего носа, резкое появление Yog-Sothoth в качестве обязательного утверждения во всех последующих обзорах кода или ретроактивном добавлении 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!

Замена метода для всех экземпляров (monkeypatching class)

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

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part 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, он может потерпеть неудачу.

Ответ 3

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

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

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

Ответ 4

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

Ответ 5

Этот ответ был включен в ответ Community Wiki

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

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

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

Ответ 6

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

Ниже приведен пример непроверенного, вероятно, ошибочного кода, но, надеюсь, демонстрирует эту идею.

например.

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; }
...
};

Ответ 7

Обычно с VTable - массив указателей на функции.

Ответ 8

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

Ответ 9

Я попробую сделать это просто:)

Мы все знаем, какие виртуальные функции находятся на С++, но как они реализованы на глубоком уровне?

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

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

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

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

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

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

Только те, у которых есть хотя бы одна виртуальная функция (пусть это даже деструктор) или выводятся хотя бы один класс с его vtable ( "является полиморфным" ).

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

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

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

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

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

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

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

Ответ 10

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

У абстрактных классов просто есть 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. Нет необходимости в vtbl для класса B.

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

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

Ответ 11

очень милое доказательство концепции я сделал немного раньше (чтобы узнать, имеет ли порядок наследования); дайте мне знать, действительно ли ваша реализация С++ отвергает его (моя версия 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 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 Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite 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.