Когда создается виртуальная таблица в C ++? - PullRequest
24 голосов
/ 26 декабря 2009

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

1) когда класс содержит хотя бы одну виртуальную функцию.

OR

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

OR

3) когда любой родительский класс на любом уровне иерархии содержит хотя бы одну виртуальную функцию.

С этим связан вопрос: Можно ли отказаться от динамической диспетчеризации в иерархии C ++?

например. рассмотрим следующий пример.

#include <iostream>
using namespace std;
class A {
public:
  virtual void f();
};
class B: public A {
public:
  void f();
};
class C: public B {
public:
  void f();
};

Какие классы будут содержать V-таблицу?

Поскольку B не объявляет f () как виртуальное, получает ли класс C динамический полиморфизм?

Ответы [ 6 ]

23 голосов
/ 26 декабря 2009

Помимо "vtables являются специфичными для реализации" (которые они есть), если используется vtable: будут уникальные vtables для каждого из ваших классов. Несмотря на то, что B :: f и C :: f не объявлены виртуальными, поскольку в базовом классе есть совпадающая сигнатура виртуального метода ( A ) в вашем коде) B :: f и C :: f оба являются неявно виртуальными. Поскольку каждый класс имеет по крайней мере один уникальный виртуальный метод ( B :: f переопределяет A :: f для B экземпляров и C :: f аналогично для C экземпляров), вам нужно три vtables.

Вам, как правило, не стоит беспокоиться о таких деталях. Важно то, есть ли у вас виртуальная диспетчеризация или нет. Вам не нужно использовать виртуальную диспетчеризацию, явно указав, какую функцию вызывать, но это обычно полезно только при реализации виртуального метода (например, для вызова базы метод). Пример: * 1 027 *

struct B {
  virtual void f() {}
  virtual void g() {}
};

struct D : B {
  virtual void f() { // would be implicitly virtual even if not declared virtual
    B::f();
    // do D-specific stuff
  }
  virtual void g() {}
};

int main() {
  {
    B b; b.g(); b.B::g(); // both call B::g
  }
  {
    D d;
    B& b = d;
    b.g(); // calls D::g
    b.B::g(); // calls B::g

    b.D::g(); // not allowed
    d.D::g(); // calls D::g

    void (B::*p)() = &B::g;
    (b.*p)(); // calls D::g
    // calls through a function pointer always use virtual dispatch
    // (if the pointed-to function is virtual)
  }
  return 0;
}

Некоторые конкретные правила, которые могут помочь; но не цитируйте их, я, вероятно, пропустил некоторые крайние случаи:

  • Если у класса есть виртуальные методы или виртуальные базы, даже если они наследуются, экземпляры должны иметь указатель vtable.
  • Если класс объявляет не унаследованные виртуальные методы (например, если у него нет базового класса), то он должен иметь свою собственную vtable.
  • Если у класса есть другой набор переопределяющих методов, чем у его первого базового класса, тогда он должен иметь свою собственную vtable и не может повторно использовать базовые. (Деструкторы обычно требуют этого.)
  • Если класс имеет несколько базовых классов, а вторая или более поздние базовые имеют виртуальные методы:
    • Если в более ранних базах не было виртуальных методов и Оптимизация пустой базы была применена ко всем более ранним базам, то обрабатывайте эту базу как первый базовый класс.
    • В противном случае класс должен иметь свой собственный vtable.
  • Если у класса есть какие-либо виртуальные базовые классы, он должен иметь свою собственную vtable.

Помните, что vtable похож на статический член данных класса, и экземпляры имеют только указатели на них.

Также см. Всеобъемлющую статью C ++: Под капотом (март 1994 г.) Джена Грея. ( Попробуйте Google , если это ссылка умирает.)

Пример повторного использования vtable:

struct B {
  virtual void f();
};
struct D : B {
  // does not override B::f
  // does not have other virtuals of its own
  void g(); // still might have its own non-virtuals
  int n; // and data members
};

В частности, обратите внимание, что dtor B не является виртуальным (и это , вероятно, ошибка в реальном коде ), но в этом примере D экземпляры будут указывать на тот же vtable, что и B экземпляры.

8 голосов
/ 26 декабря 2009

Ответ: «Это зависит». Это зависит от того, что вы подразумеваете под «содержать vtbl», и от решений, принятых разработчиком конкретного компилятора.

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

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

Если вы спросите: «Что делает GCC?» или «Что делает Visual C ++?» тогда вы могли бы получить конкретный ответ.

@ Ответ Хасана Сайеда, вероятно, ближе к тому, о чем вы спрашивали, но очень важно сохранить концепции прямо здесь.

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

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

С точки зрения реализации, компилятору разрешено делать все, что он хочет для достижения этого результата.

7 голосов
/ 26 декабря 2009

Ответ

vtable создается, когда объявление класса содержит виртуальную функцию. Vtable вводится, когда родитель - где-нибудь в иерархии - имеет виртуальную функцию, давайте вызовем этого родителя Y. У любого родителя Y не будет vtable (если у них нет virtual для какой-то другой функции в их иерархии ).

Читайте дальше для обсуждения и тестирования

- объяснение -

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

Вы не несете стоимость виртуальной таблицы, если избегаете виртуального ключевого слова.

- редактировать: отразить ваши изменения -

Только когда базовый класс содержит виртуальную функцию, любые другие подклассы содержат vtable. Родители указанного базового класса не имеют vtable.

В вашем примере все три класса будут иметь vtable, потому что вы можете попробовать использовать все три класса через A*.

- тест - GCC 4+ -

#include <iostream>

class test_base
{
  public:
    void x(){std::cout << "test_base" << "\n"; };
};

class test_sub : public test_base
{
public:
  virtual void x(){std::cout << "test_sub" << "\n"; } ;
};

class test_subby : public test_sub
{
public:
  void x() { std::cout << "test_subby" << "\n"; }
};

int main() 
{
  test_sub sub;
  test_base base;
  test_subby subby;

  test_sub * psub;
  test_base *pbase;
  test_subby * psubby;

  pbase = &sub;
  pbase->x();
  psub = &subby;
  psub->x();

  return 0;
}

выход

test_base
test_subby

test_base не имеет виртуальной таблицы, поэтому все, что будет к ней применено, будет использовать x() из test_base. test_sub, с другой стороны, меняет природу x(), и его указатель будет косвенным через vtable, и это показывает, что test_subby x() выполняется.

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

3 голосов
/ 26 декабря 2009

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

  • Когда объект получает неявный указатель на V-таблицу, вставленную в него?

или

  • Когда выделяется , отдельная V-таблица, созданная для данного типа в иерархии?

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

Ответ на второй вопрос может быть таким же, как и на первый (вариант 3), с возможным исключением. Если у некоторого полиморфного класса в иерархии с одним наследованием нет собственных виртуальных функций (нет новых виртуальных функций, нет переопределений для родительской виртуальной функции), возможно, что реализация решит не создавать отдельную V-таблицу для этого класса, а вместо этого используйте V-таблицу своего непосредственного родителя и для этого класса (поскольку она все равно будет одинаковой). То есть в этом случае и объекты родительского типа, и объекты производного типа будут хранить одинаковое значение во встроенных указателях V-таблицы. Это, конечно, сильно зависит от реализации. Я проверил GCC и MS VS 2005, и они не действуют таким образом. Они оба создают отдельную V-таблицу для производного класса в этой ситуации, но я, кажется, вспоминаю, что слышал о реализациях, которые этого не делают.

2 голосов
/ 26 декабря 2009
Стандарты

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

1 голос
/ 26 декабря 2009

Поведение определено в параграфе 2 главы 10.3 спецификации языка C ++:

Если виртуальная функция-член vf объявлено в классе Base и в Производный класс, полученный напрямую или косвенно из базы, член функция VF с тем же именем и тот же список параметров, что и Base :: vf объявлено, тогда Derived :: vf также виртуальный ( независимо от того, так ли это объявлен ) и переопределяет Base :: vf.

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

...