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

Конструкторы С++: почему вызов этой виртуальной функции небезопасен?

Это из стандарта С++ 11 с 12.7.4. Это довольно запутанно.

  • Что означает последнее предложение в тексте?
  • Почему последний вызов метода в B::B undefined? Shoudn't это просто называть a.A::f?

4 Функции-члены, включая виртуальные функции (10.3), можно назвать во время строительства или уничтожения (12.6.2). Когда виртуальная функция вызывается прямо или косвенно из конструктора или из деструктор, в том числе при строительстве или классы нестатические элементы данных и объект, которому вызов применяется объект (назовите его x) под строительство или уничтожение, вызываемая функция является конечным переопределением в конструкторах или класс деструкторов, а не один, переопределяющий его в более производном классе. Если вызов виртуальной функции использует доступ к явным членам класса (5.2.5), а выражение объекта относится к полному объекту x или один из этих подобъектов базового класса, но не x или один из его субобъекты базового класса, поведение undefined. [Пример:

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

struct A : virtual V {
 virtual void f();
};

struct B : virtual V {
 virtual void g();
 B(V*, A*);
};

struct D : A, B {
 virtual void f();
 virtual void g();
 D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
 f(); // calls V::f, not A::f
 g(); // calls B::g, not D::g
 v->g(); // v is base of B, the call is well-defined, calls B::g
 a->f(); // undefined behavior, a’s type not a base of B
}

-end пример]

4b9b3361

Ответ 1

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

Например, рассмотрим эту диаграмму наследования (стрелки указывают из производных классов на базовые классы)

enter image description here

Скажем, мы строим "большой" объект типа J. И в настоящее время мы выполняем конструктор класса H. Внутри конструктора H вам разрешено пользоваться типичным ограниченным конструктором полиморфизмом суб-иерархии внутри красного овала. Например, вы можете вызывать виртуальные функции базового подобъекта типа B, а полиморфное поведение будет работать, как и ожидалось, внутри обведенной иерархии ( "как ожидалось" ) означает, что полиморфное поведение будет меньше, чем H в иерархии, но не ниже). Вы также можете вызвать виртуальные функции A, E, X и другие подобъекты, попадающие в красный овал.

Однако, если вы каким-то образом получаете доступ к иерархии вне овала и пытаетесь использовать там полиморфизм, поведение становится undefined. Например, если вы каким-то образом получаете доступ к подобъекту G из конструктора H и пытаетесь вызвать виртуальную функцию G - поведение undefined. То же самое можно сказать о вызове виртуальных функций D и I из конструктора H.

Единственный способ получить такой доступ к "внешней" суб-иерархии - это если кто-то каким-то образом передал указатель/ссылку на подобъект G в конструктор H. Следовательно, ссылка на "явный доступ к членам класса" в стандартном тексте (хотя это кажется чрезмерным).

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

Обратите внимание, что это ограничение применяется, даже если конструкция субъектов D, G и I закончена до начала построения H.


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

Взгляните на пример из стандарта. Класс A выводится из класса V. Это означает, что указатель VMT A физически относится к подобъекту V. Все вызовы виртуальных функций, введенные V, отправляются через указатель VMT, введенный V. То есть когда вы вызываете

pointer_to_A->f();

он фактически переведен в

V *v_subobject = (V *) pointer_to_A; // go to V
vmt = v_subobject->vmt_ptr;          // retrieve the table
vmt[index_for_f]();                  // call through the table

Однако в примере из стандарта тот же самый подобъект V также встроен в B. Чтобы сделать корректный полиморфизм, ограниченный конструктором, компилятор поместит указатель на B VMT в указатель VMT, хранящийся в V (поскольку в то время как конструктор B активен V подобъект должен действовать как часть B).

Если в этот момент вы как-то пытаетесь вызвать

a->f(); // as in the example

приведенный выше алгоритм найдет B указатель VMT, сохраненный в подобъекте V, и попытается вызвать f() через этот VMT. Это явно не имеет никакого смысла. То есть с виртуальными методами A, отправленными через B VMT, нет смысла. Поведение undefined.

Это довольно просто проверить с помощью практического эксперимента. Пусть добавьте свою собственную версию f в B и сделайте это

#include <iostream>

struct V {
  virtual void f() { std::cout << "V" << std::endl; }
};

struct A : virtual V {
  virtual void f() { std::cout << "A" << std::endl; }
};

struct B : virtual V {
  virtual void f() { std::cout << "B" << std::endl; }
  B(V*, A*);
};

struct D : A, B {
  virtual void f() {}
  D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
  a->f(); // What `f()` is called here???
}

int main() {
  D d;
}

Вы ожидаете, что здесь будет вызываться A::f? Я попробовал несколько компиляторов, все они на самом деле называют B::f! Между тем, значение указателя this B::f, принимаемое в таком вызове, является полностью фиктивным.

http://ideone.com/Ua332

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

Можно заметить, что в этом конкретном примере фактически виртуальное наследование приводит к этому необычному поведению. Да, это происходит именно потому, что подобъект V делится между подобъектами A и B. Вполне возможно, что без виртуального наследования поведение было бы гораздо более предсказуемым. Однако спецификация языка, по-видимому, решила просто нарисовать линию так, как она нарисована на моей диаграмме: когда вы строите H, вам не удастся выйти из "песочницы" суб-иерархии H, независимо от того, какой тип наследования используется.

Ответ 2

Вот как я это понимаю: во время построения объекта каждый под-объект строит свою часть. В этом примере это означает, что V::V() инициализирует членов V; A инициализирует члены A и т.д. Поскольку V инициализируется до A и B, они могут полагаться на члены V для инициализации.

В этом примере конструктор B принимает два указателя на себя. Его часть V уже построена, поэтому можно безопасно вызвать v->g(). Однако в этот момент часть D A еще не была инициализирована. Поэтому вызов a->f() получает доступ к неинициализированной памяти, которая является undefined.

Edit:

В D выше, A инициализируется до B, поэтому не будет доступа к A неинициализированной памяти. С другой стороны, после того, как A был полностью построен, его виртуальные функции переопределены символами D (на практике: его vtable имеет значение A во время построения и D после завершения построения). Поэтому вызов a->f() вызывает D::f(), прежде чем D будет инициализирован. Итак, в любом случае - A построено до B или после - вы собираетесь вызвать метод на неинициализированном объекте.

Часть виртуальных функций уже обсуждалась здесь, но для полноты: вызов f() использует V::f, потому что A еще не инициализирован, а в отношении B - единственное реализация f. g() вызывает B::g, потому что B переопределяет g.

Ответ 3

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

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

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

В примере содержится множественное наследование: D происходит от A и B (мы проигнорируем V, потому что не требуется доказывать, почему поведение undefined). Во время построения объекта D как конструкторы A, так и B будут вызываться для создания субобъектов базового класса объекта D.

Когда вызывается конструктор B, тип полного объекта x равен D. В этом конструкторе A является указателем на подобъект базового класса A x. Итак, мы можем сказать следующее о a->f():

  • Строящийся объект представляет собой субобъект базового класса B объекта D (поскольку подобъект этого базового класса является объектом, который в настоящее время строится, это то, что текст называется x).

  • Он использует явный доступ к членам класса (в этом случае оператор ->)

  • Тип полного объекта x - D, потому что это наиболее производный тип, который строится

  • Объектное выражение (A) относится к подобъекту базового класса для полного объекта x (он ссылается на подобъект базового класса A объекта D)

  • Субобъект базового класса, к которому относится выражение объекта, не является x и не является подобъектом базового класса x: A не B и A не является базовым классом B.

Следовательно, поведение вызова undefined, по правилу, с которого мы начали с самого начала.

Почему последний вызов метода в B::B undefined? Не следует ли просто вызвать a.A::f?

Правило, которое вы цитируете, гласит, что когда конструктор вызывается во время построения, "вызванная функция является окончательным переопределением в классе конструкторов, а не переопределяет его в более производном классе".

В этом случае класс конструктора B. Поскольку B не выводится из A, для виртуальной функции нет окончательного переопределения. Поэтому попытка сделать виртуальный вызов демонстрирует поведение undefined.