Подтвердить что ты не робот

Что такое VTT для класса?

Недавно я столкнулся с ошибкой компоновщика С++, которая была для меня новой.

libfoo.so: undefined reference to `VTT for Foo'
libfoo.so: undefined reference to `vtable for Foo'

Я распознал ошибку и исправил свою проблему, но у меня все еще есть вопрос: что такое VTT?

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

4b9b3361

Ответ 1

Страница "Заметки о множественном наследовании в компиляторе GCC С++ v4.0.1" теперь находится в автономном режиме и http://web.archive.org не архивировал его. Итак, я нашел копию текста в tinydrblog, который заархивирован в веб-архиве.

Существует полный текст оригинальных заметок, опубликованных в Интернете в разделе "Семинар по докторантуре: внутренние языки GCC "(осень 2005 г.) выпускник Morgan Deters "в лаборатории распределенных вычислений объектов в отделе компьютерных наук в Вашингтонском университете в Сент-Луисе."
Его (заархивированная) домашняя страница:

THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. PART1:

Основы: одиночное наследование

Как мы обсуждали в классе, одиночное наследование приводит к компоновке объекта с данными базового класса, выложенными перед данными производного класса. Поэтому, если классы A и B определяются следующим образом:

class A {
public:
  int a;

};

class B : public A {
public:
  int b;
};

тогда объекты типа B выкладываются следующим образом (где "b" является указателем на такой объект):

b --> +-----------+
      |     a     |
      +-----------+
      |     b     |
      +-----------+

Если у вас есть виртуальные методы:

class A {
public:
  int a;
  virtual void v();
};

class B : public A {
public:
  int b;
};

то у вас также будет указатель vtable:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
b --> +----------+         | ptr to typeinfo for B |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+
      |     b    |
      +----------+

то есть top_offset и указатель типаinfo живут над местом, в которое указывает указатель vtable.

Простое множественное наследование

Теперь рассмотрим множественное наследование:

class A {
public:
  int a;
  virtual void v();
};

class B {
public:
  int b;
  virtual void w();
};

class C : public A, public B {
public:
  int c;
};

В этом случае объекты типа C выкладываются следующим образом:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
c --> +----------+         | ptr to typeinfo for C |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |    -8 (top_offset)    |
      |  vtable  |---+     +-----------------------+
      +----------+   |     | ptr to typeinfo for C |
      |     b    |   +---> +-----------------------+
      +----------+         |         B::w()        |
      |     c    |         +-----------------------+
      +----------+

... но почему? Почему два vtables в одном? Ну, подумайте о замене типа. Если у меня есть указатель на C, я могу передать его функции, которая ожидает от указателя к-A или функции, которая ожидает от указателя к-B. Если функция ожидает указатель-на-A, и я хочу передать ему значение моей переменной c (типа pointer-to-C), я уже настроен. Вызовы A::v() могут быть выполнены с помощью (первой) таблицы vtable, и вызываемая функция может получить доступ к элементу a через указатель, который я передаю, таким же образом, как он может через любой указатель-на-A.

Однако, если я передам значение моей переменной-указателю c функции, которая ожидает указателя-на-B, нам также понадобится подобъект типа B в нашем C для ссылки на него. Вот почему у нас есть второй указатель vtable. Мы можем передать значение указателя (c + 8 байтов) функции, ожидающей от указателя к-B, и все это задано: он может совершать вызовы B::w() через (v) указатель (второй) vtable и обращаться к члену b через указатель мы проходим так же, как и через любой указатель-на-B.

Обратите внимание, что эта "коррекция указателя" также должна возникать для вызываемых методов. Класс c наследует B::w() в этом случае. Когда w() вызывается через указатель-на-C, указатель (который становится этим указателем внутри w()) должен быть скорректирован. Это часто называют этой корректировкой указателя.

В некоторых случаях компилятор будет генерировать thunk для исправления адреса. Рассмотрим тот же код, что и выше, но на этот раз c переопределяет B функцию-член w():

class A {
public:
  int a;
  virtual void v();
};

class B {
public:
  int b;
  virtual void w();
};

class C : public A, public B {
public:
  int c;
  void w();
};

c layout объекта и vtable теперь выглядят следующим образом:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
c --> +----------+         | ptr to typeinfo for C |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |         C::w()        |
      |  vtable  |---+     +-----------------------+
      +----------+   |     |    -8 (top_offset)    |
      |     b    |   |     +-----------------------+
      +----------+   |     | ptr to typeinfo for C |
      |     c    |   +---> +-----------------------+
      +----------+         |    thunk to C::w()    |
                           +-----------------------+

Теперь, когда w() вызывается в экземпляре c через указатель-на-B, вызывается thunk. Что делает тнк? Разберите его (здесь, с gdb):

0x0804860c <_ZThn8_N1C1wEv+0>:  addl   $0xfffffff8,0x4(%esp)
0x08048611 <_ZThn8_N1C1wEv+5>:  jmp    0x804853c <_ZN1C1wEv>

Поэтому он просто настраивает указатель this и переходит на C::w(). Все хорошо.

Но не означает ли это, что B vtable всегда указывает на этот C::w() thunk? Я имею в виду, если у нас есть указатель-на-B, который является законным B (а не c), мы не хотим вызывать thunk, правильно?

Right. Вышеприведенная встроенная таблица vtable для B в c является специальной для случая B-in-C. B обычный vtable является нормальным и непосредственно указывает на B::w().

Алмаз: множественные копии базовых классов (не виртуальное наследование)

Хорошо. Теперь нужно заняться действительно тяжелым делом. Вспомним обычную проблему множественных копий базовых классов при формировании алмаза наследования:

class A {
public:
  int a;
  virtual void v();
};

class B : public A {
public:
  int b;
  virtual void w();
};

class C : public A {
public:
  int c;
  virtual void x();
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

Обратите внимание, что D наследует как от B, так и c, а B и c оба наследуются от A. Это означает, что D имеет две копии A. Макет объекта и встраивание vtable - это то, чего мы ожидаем от предыдущих разделов:

                           +-----------------------+
                           |     0 (top_offset)    |
                           +-----------------------+
d --> +----------+         | ptr to typeinfo for D |
      |  vtable  |-------> +-----------------------+
      +----------+         |         A::v()        |
      |     a    |         +-----------------------+
      +----------+         |         B::w()        |
      |     b    |         +-----------------------+
      +----------+         |         D::y()        |
      |  vtable  |---+     +-----------------------+
      +----------+   |     |   -12 (top_offset)    |
      |     a    |   |     +-----------------------+
      +----------+   |     | ptr to typeinfo for D |
      |     c    |   +---> +-----------------------+
      +----------+         |         A::v()        |
      |     d    |         +-----------------------+
      +----------+         |         C::x()        |
                           +-----------------------+

Конечно, мы ожидаем, что данные A (член A) будут существовать дважды в макете объектов D (и это так), и мы ожидаем, что A виртуальные функции-члены будут представлены дважды в таблице vtable (и A::v() действительно существует). Ладно, здесь ничего нового.

Алмаз: одиночные копии виртуальных баз

Но что, если мы будем применять виртуальное наследование? Виртуальное наследование С++ позволяет нам указывать иерархию алмазов, но гарантируется только одна копия фактически унаследованных баз. Поэтому напишите наш код следующим образом:

class A {
public:
  int a;
  virtual void v();
};

class B : public virtual A {
public:
  int b;
  virtual void w();
};

class C : public virtual A {
public:
  int c;
  virtual void x();
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

Внезапно все становится намного сложнее. Если в нашем представлении D мы можем иметь только одну копию A, то мы уже не можем уйти от нашего "трюка" вложения a c в D (и вложение vtable для c часть D в D vtable). Но как мы можем обрабатывать обычную подстановку типов, если мы не можем это сделать?

Попробуйте установить схему:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+                 |         C::x()        |
      |  vtable  |----+            +-----------------------+
      +----------+    |            |    0 (vbase_offset)   |
      |     a    |    |            +-----------------------+
      +----------+    |            |   -20 (top_offset)    |
                      |            +-----------------------+
                      |            | ptr to typeinfo for D |
                      +----------> +-----------------------+
                                   |         A::v()        |
                                   +-----------------------+

Хорошо. Итак, вы видите, что A теперь встроен в D по существу так же, как и другие базы. Но он встроен в D, а не inits непосредственно производные классы.

Ответ 2

THIS IS THE TEXT by Morgan Deters and NOT CC-licensed. PART2:

Конструкция/разрушение в присутствии множественного наследования

Как этот объект построен в памяти при построении самого объекта? И как мы гарантируем, что частично построенный объект (и его vtable) безопасен для конструкторов для работы?

К счастью, все это было очень осторожно для нас. Предположим, мы создаем новый объект типа D (через, например, new D). Во-первых, память для объекта выделяется в куче и возвращается указатель. Вызывается конструктор D, но перед выполнением любой D -специфической конструкции он вызывает конструктор A на объекте (после настройки курсора this, конечно!). Конструктор A заполняет часть A объекта D, как если бы это был экземпляр A.

d --> +----------+
      |          |
      +----------+
      |          |
      +----------+
      |          |
      +----------+
      |          |       +-----------------------+
      +----------+       |     0 (top_offset)    |
      |          |       +-----------------------+
      +----------+       | ptr to typeinfo for A |
      |  vtable  |-----> +-----------------------+
      +----------+       |         A::v()        |
      |    a     |       +-----------------------+
      +----------+

Элемент управления возвращается в конструктор D, который вызывает конструктор B. (Здесь не требуется настройка указателя.) Когда конструктор B сделан, объект выглядит следующим образом:

                                             B-in-D
                          +-----------------------+
                          |   20 (vbase_offset)   |
                          +-----------------------+
                          |     0 (top_offset)    |
                          +-----------------------+
d --> +----------+        | ptr to typeinfo for B |
      |  vtable  |------> +-----------------------+
      +----------+        |         B::w()        |
      |    b     |        +-----------------------+
      +----------+        |    0 (vbase_offset)   |
      |          |        +-----------------------+
      +----------+        |   -20 (top_offset)    |
      |          |        +-----------------------+
      +----------+        | ptr to typeinfo for B |
      |          |   +--> +-----------------------+
      +----------+   |    |         A::v()        |
      |  vtable  |---+    +-----------------------+
      +----------+
      |    a     |
      +----------+

Но ждать... B конструктор изменил часть объекта A, изменив его указатель vtable! Как он знал, чтобы отличить этот тип B-in-D от B-in-something-else (или автономного B, если на то пошло)? Просто. Таблица виртуальной таблицы сообщила ему об этом. Эта структура, сокращенно VTT, представляет собой таблицу vtables, используемую при построении. В нашем случае VTT для D выглядит следующим образом:

                                                                  B-in-D
                                               +-----------------------+
                                               |   20 (vbase_offset)   |
            VTT for D                          +-----------------------+
+-------------------+                          |     0 (top_offset)    |
|    vtable for D   |-------------+            +-----------------------+
+-------------------+             |            | ptr to typeinfo for B |
| vtable for B-in-D |-------------|----------> +-----------------------+
+-------------------+             |            |         B::w()        |
| vtable for B-in-D |-------------|--------+   +-----------------------+
+-------------------+             |        |   |    0 (vbase_offset)   |
| vtable for C-in-D |-------------|-----+  |   +-----------------------+
+-------------------+             |     |  |   |   -20 (top_offset)    |
| vtable for C-in-D |-------------|--+  |  |   +-----------------------+
+-------------------+             |  |  |  |   | ptr to typeinfo for B |
|    vtable for D   |----------+  |  |  |  +-> +-----------------------+
+-------------------+          |  |  |  |      |         A::v()        |
|    vtable for D   |-------+  |  |  |  |      +-----------------------+
+-------------------+       |  |  |  |  |
                            |  |  |  |  |                         C-in-D
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      |   12 (vbase_offset)   |
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      |     0 (top_offset)    |
                            |  |  |  |  |      +-----------------------+
                            |  |  |  |  |      | ptr to typeinfo for C |
                            |  |  |  |  +----> +-----------------------+
                            |  |  |  |         |         C::x()        |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         |    0 (vbase_offset)   |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         |   -12 (top_offset)    |
                            |  |  |  |         +-----------------------+
                            |  |  |  |         | ptr to typeinfo for C |
                            |  |  |  +-------> +-----------------------+
                            |  |  |            |         A::v()        |
                            |  |  |            +-----------------------+
                            |  |  |
                            |  |  |                                    D
                            |  |  |            +-----------------------+
                            |  |  |            |   20 (vbase_offset)   |
                            |  |  |            +-----------------------+
                            |  |  |            |     0 (top_offset)    |
                            |  |  |            +-----------------------+
                            |  |  |            | ptr to typeinfo for D |
                            |  |  +----------> +-----------------------+
                            |  |               |         B::w()        |
                            |  |               +-----------------------+
                            |  |               |         D::y()        |
                            |  |               +-----------------------+
                            |  |               |   12 (vbase_offset)   |
                            |  |               +-----------------------+
                            |  |               |    -8 (top_offset)    |
                            |  |               +-----------------------+
                            |  |               | ptr to typeinfo for D |
                            +----------------> +-----------------------+
                               |               |         C::x()        |
                               |               +-----------------------+
                               |               |    0 (vbase_offset)   |
                               |               +-----------------------+
                               |               |   -20 (top_offset)    |
                               |               +-----------------------+
                               |               | ptr to typeinfo for D |
                               +-------------> +-----------------------+
                                               |         A::v()        |
                                               +-----------------------+
Конструктор

D передает указатель на конструктор D VTT в B (в этом случае он переходит в адрес первой записи B-in-D). И действительно, vtable, который использовался для компоновки объекта выше, является специальной таблицей vtable, используемой только для построения B-in-D.

Элемент управления возвращается конструктору D, и он вызывает конструктор C (с параметром адреса VTT, указывающим на запись "C-in-D + 12" ). Когда конструктор C выполняется с объектом, он выглядит следующим образом:

                                                                           B-in-D
                                                        +-----------------------+
                                                        |   20 (vbase_offset)   |
                                                        +-----------------------+
                                                        |     0 (top_offset)    |
                                                        +-----------------------+
                                                        | ptr to typeinfo for B |
                    +---------------------------------> +-----------------------+
                    |                                   |         B::w()        |
                    |                                   +-----------------------+
                    |                          C-in-D   |    0 (vbase_offset)   |
                    |       +-----------------------+   +-----------------------+
d --> +----------+  |       |   12 (vbase_offset)   |   |   -20 (top_offset)    |
      |  vtable  |--+       +-----------------------+   +-----------------------+
      +----------+          |     0 (top_offset)    |   | ptr to typeinfo for B |
      |    b     |          +-----------------------+   +-----------------------+
      +----------+          | ptr to typeinfo for C |   |         A::v()        |
      |  vtable  |--------> +-----------------------+   +-----------------------+
      +----------+          |         C::x()        |
      |    c     |          +-----------------------+
      +----------+          |    0 (vbase_offset)   |
      |          |          +-----------------------+
      +----------+          |   -12 (top_offset)    |
      |  vtable  |--+       +-----------------------+
      +----------+  |       | ptr to typeinfo for C |
      |    a     |  +-----> +-----------------------+
      +----------+          |         A::v()        |
                            +-----------------------+

Как вы видите, конструктор C снова модифицировал встроенный указатель vtable. Встраиваемые объекты C и A теперь используют специальную конструкцию C-in-D vtable, а встроенный объект B использует специальную конструкцию B-in- D vtable. Наконец, конструктор D завершает работу, и мы получаем ту же диаграмму, что и раньше:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+                 |         C::x()        |
      |  vtable  |----+            +-----------------------+
      +----------+    |            |    0 (vbase_offset)   |
      |     a    |    |            +-----------------------+
      +----------+    |            |   -20 (top_offset)    |
                      |            +-----------------------+
                      |            | ptr to typeinfo for D |
                      +----------> +-----------------------+
                                   |         A::v()        |
                                   +-----------------------+

Разрушение происходит таким же образом, но наоборот. D destructor. После запуска кода уничтожения пользователя деструктор вызывает C destructor и направляет его на использование соответствующей части D VTT. C destructor манипулирует указателями vtable так же, как во время строительства; то есть соответствующие указатели vtable теперь указывают на конструкцию v-таблицы C-in-D. Затем он запускает код уничтожения пользователя для C и возвращает управление D-деструктору, который затем вызывает B-деструктор B со ссылкой на D VTT. B деструктор устанавливает соответствующие части объекта для ссылки на конструкцию v-таблицы B-in-D. Он запускает код уничтожения пользователя для B и возвращает управление деструктору D, который, наконец, вызывает деструктор. Деструктор изменяет vtable для части A объекта, чтобы ссылаться на vtable для A. Наконец, управление возвращается к D деструктору и уничтожение объекта завершено. Память, однажды использованная объектом, возвращается в систему.

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

Конструктор "in-charge" (или полный объект) - это тот, который строит виртуальные базы, а конструктор "незапланированный" (или базовый объект) - это тот, который этого не делает. Рассмотрим наш пример. Если построено B, его конструктор должен вызвать конструктор A для его построения. Аналогично, конструктору C необходимо построить A. Однако, если B и C построены как часть конструкции D, их конструкторы не должны строить A, поскольку A является виртуальной базой, а конструктор D позаботится о его построении ровно один раз для экземпляра D. Рассмотрим случаи:

Если вы создаете новый A, для построения A. вызывается конструктор "in-charge" . Когда вы создаете новый B, вызывается конструктор B "in-charge" . Он будет называть конструктор "не в заряда" для A.

новый C похож на новый B.

Новый D вызывает конструктор D "in-charge" . Провели этот пример. D "in-charge" конструктор вызывает "не-ответственные" версии конструкторов A, B и C (в том порядке).

Деструктор "in-charge" - это аналог "встроенного" конструктора - он берет на себя ответственность за разрушение виртуальных баз. Аналогичным образом создается деструктор "не в заряда". Но есть и третий. Деструктор "с избыточной зарядкой" - это тот, который освобождает хранилище, а также разрушает объект. Итак, когда кто-то называется предпочтительнее другого?

Ну, есть два типа объектов, которые могут быть уничтожены - те, которые выделены в стеке, и те, которые выделены в куче. Рассмотрим этот код (с учетом нашей алмазной иерархии с виртуальным наследованием от ранее):

D d;            // allocates a D on the stack and constructs it
D *pd = new D;  // allocates a D in the heap and constructs it
/* ... */
delete pd;      // calls "in-charge deleting" destructor for D
return;         // calls "in-charge" destructor for stack-allocated D

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

Ну, компилятор мог бы сделать такую ​​вещь, но это было бы более сложным по другим причинам. Рассмотрим этот код (предполагая виртуальный деструктор, который вы всегда используете, правильно?... правильно?!?):

D *pd = new D;  // allocates a D in the heap and constructs it
C *pc = d;      // we have a pointer-to-C that points to our heap-allocated D
/* ... */
delete pc;      // call destructor thunk through vtable, but what about delete?

Если у вас не было класса "деструктор", "удаление заряда", то операция удаления должна была бы отрегулировать указатель так же, как и деструктор thunk. Помните, что объект C встроен в D, и поэтому наш указатель-на-C выше настроен так, чтобы указывать на середину нашего объекта D. Мы не можем просто удалить этот указатель, так как это не указатель, который был возвращенный malloc(), когда мы его построили.

Итак, если у нас не было дезактивирующего деструктора, мы должны были бы использовать thunks для оператора delete (и представлять их в наших vtables) или что-то еще подобное.

Thunks, виртуальный и не виртуальный

Этот раздел еще не написан.

Множественное наследование с помощью виртуальных методов на одной стороне

Хорошо. Последнее упражнение. Что, если у нас есть иерархия наследования алмазов с виртуальным наследованием, как и раньше, но только с виртуальными методами по одной стороне? Итак:

class A {
public:
  int a;
};

class B : public virtual A {
public:
  int b;
  virtual void w();
};

class C : public virtual A {
public:
  int c;
};

class D : public B, public C {
public:
  int d;
  virtual void y();
};

В этом случае макет объекта выглядит следующим образом:

                                   +-----------------------+
                                   |   20 (vbase_offset)   |
                                   +-----------------------+
                                   |     0 (top_offset)    |
                                   +-----------------------+
                                   | ptr to typeinfo for D |
                      +----------> +-----------------------+
d --> +----------+    |            |         B::w()        |
      |  vtable  |----+            +-----------------------+
      +----------+                 |         D::y()        |
      |     b    |                 +-----------------------+
      +----------+                 |   12 (vbase_offset)   |
      |  vtable  |---------+       +-----------------------+
      +----------+         |       |    -8 (top_offset)    |
      |     c    |         |       +-----------------------+
      +----------+         |       | ptr to typeinfo for D |
      |     d    |         +-----> +-----------------------+
      +----------+
      |     a    |
      +----------+

Итак, вы можете видеть подобъект C, который не имеет виртуальных методов, все еще имеет vtable (хотя и пустой). Действительно, все экземпляры C имеют пустую vtable.

Спасибо, Morgan Deters!!

Ответ 3

Виртуальная таблица таблицы (VTT). Он позволяет безопасно создавать/деконструировать объекты при множественном наследовании.

для объяснения см.: http://www.cse.wustl.edu/~mdeters/seminar/fall2005/mi.html

Ответ 6

Таблица виртуальной таблицы, сокращенно VTT, представляет собой таблицу vtables, используемую при построении, где задействовано множественное наследование.

Дополнительная информация здесь в этой интересной статье: Заметки о множественном наследовании в компиляторе GCC С++ v4.0.1