Как реализуются виртуальные функции и vtable? - PullRequest
99 голосов
/ 19 сентября 2008

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

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

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

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

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

Ответы [ 12 ]

115 голосов
/ 19 сентября 2008

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Я не верю, что время выполнения переопределенной виртуальной функции уменьшается по сравнению с вызовом базовой виртуальной функции. Однако для класса есть дополнительные затраты пространства, связанные с определением другой 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

28 голосов
/ 19 сентября 2008
  • Может ли 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, он может потерпеть неудачу.

17 голосов
/ 09 апреля 2015

Замедляет ли наличие одной виртуальной функции весь класс?

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

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

2 голосов
/ 14 апреля 2015

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

Мы все знаем, что такое виртуальные функции в 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 голосов
/ 19 сентября 2008

Вы можете воссоздать функциональность виртуальных функций в 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; }
...
};
2 голосов
/ 19 сентября 2008

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

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

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

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

2 голосов
/ 19 сентября 2008

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

1 голос
/ 26 июля 2018

Вот запускаемая ручная реализация виртуальной таблицы в современном 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 голос
/ 12 апреля 2015

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

1 голос
/ 19 сентября 2008

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

...