Влияние виртуального наследования на конструктор - PullRequest
2 голосов
/ 11 января 2020

Я начал изучать виртуальное наследование (и как оно может решить проблемы с классом, производным от двух родительских классов с одним и тем же родителем). Чтобы лучше понять механизм, стоящий за ним, я сделал следующий пример:

class A {
public: 
    A(string text = "Constructor A") { cout << text << endl; }
};

class B:  public A {
public:
    B(): A("A called from B") { cout << "Constructor B" << endl; }
};

class C : virtual public A {
public:
    C() : A("A called from C") { cout << "Constructor C" << endl; }
};

class D :  public B,  public C {
public: 
    D() { cout << "Constructor D" << endl; }
};

У меня есть class A, class B, полученное из A, class C, фактически полученное из A, и class D получено из B и C. В основном я просто формирую объект class D: D d; и получаю следующий вывод

Constructor A
A called from B
Constructor B
Constructor C
Constructor D

Что меня беспокоит, так это почему "Конструктор A" сигнализирует, что он не вызывается ни class B, ни C. И почему нет "A, вызываемого из C" до "Конструктор C" . Что касается последнего, то я знаю, что он связан с виртуальным производным class C, поэтому я думаю, что он больше не вызывает Constructor A, поскольку объект из class A уже сформирован (фактически два раза).

РЕДАКТИРОВАТЬ:

В основном я просто сделать один тип объекта D.

int main() {
  D d;
}

Ответы [ 3 ]

4 голосов
/ 11 января 2020

Во-первых, поскольку B происходит от A не виртуально, D заканчивается двумя A подобъектами (как если бы вы вообще не использовали virtual наследование).

Не виртуальный A создается B() (следовательно, "A called from B"), а виртуальный печатает "Constructor A" при создании.

Это потому, что конструкторы виртуальных (возможно, косвенных) базисов всегда вызывается конструктором самого производного класса (D), а не конструкторами каких-либо промежуточных баз (C).

Это означает, что : A("A called from C") в C() игнорируется ( так как вы не создаете автономный C). И поскольку D() не упоминает A в своем списке инициализаторов, виртуальный A создается без каких-либо параметров (как будто по : A()).


Также стоит упоминая, что все виртуальные базы созданы до не виртуальных.

Таким образом, ваш код гарантированно напечатает Constructor A до A called from B.

3 голосов
/ 11 января 2020

Проблема

Ваш код что-то забыл, и у вас все еще есть два A объекта в D объекте. Вы можете проверить это утверждение, добавив члена * publi c int test к A и попробуйте следующий код. Вы получите два разных адреса:

D d;
cout << &d.B::test <<endl;   // the non virtual A subobject of B
cout << &d.C::test <<endl;   // the virtual A subobject of C

Демо-версия

Решение

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

class B:   virtual public A {...}  // you forgot the virtual here 

Приведенные выше фрагменты кода будут работать, как и ожидалось, и вы даже можете обратиться к d.test, не получив ошибки двусмысленности. Онлайн-демонстрация

Редактирование: деликатная конструкция A

Правила C ++ требуют, чтобы каждый объект с виртуальным подобъектом A предоставлял конструктор для A , В вашем случае D должен предусматривать конструкцию virual A.

Поскольку явного конструктора нет, D будет искать конструкцию по умолчанию A. И он находит его, поскольку вы предоставляете аргумент по умолчанию для конструктора A. Это вводит в заблуждение, потому что он говорит вам «конструктор», когда на самом деле это был D, который использовал его.

Если вы удалите этот аргумент по умолчанию, ваш код больше не будет компилироваться. Затем вам нужно что-то вроде этого, чтобы правильно построить A:

class A {
public: 
    int test; 
    A(string text) { cout << "A is constructed: "<<text << endl; }
};

class D :  public B,  public C {
public: 
    D() : A("Mandatory, if there is no default consructor") { cout << "Constructor D" << endl; }
};

Демонстрационная версия

Почему правила построения C ++ такие? Потому что, когда у вас есть виртуальное наследование, нет никакой причины, что конструкция A с помощью B отрисовывается поверх конструкции на C. Ни наоборот. С другой стороны, и B, и C определяют, как создать свой субобъект A. Чтобы решить неоднозначность в выборе правильной конструкции, было принято это правило. И это может быть болезненно, если виртуальный класс не имеет конструктора по умолчанию.

2 голосов
/ 11 января 2020

Когда тип имеет виртуальную базу, виртуальная база создается из конструктора самого производного типа. Поэтому «Конструктор A» вызывается из конструктора D.

Немного подробнее:

#include <iostream>

struct base {
    base() { std::cout << "base()\n"; }
    base(int) { std::cout << "base(int)\n"; }
};

struct i1 : virtual base {
    i1() : base(0) { std::cout << "i1()\n"; }
};

struct i2 : virtual base {
    i2() : base(1) { std::cout << "i2()\n"; }
};

struct d : i1, i2 {
};

Теперь, если код создает объект типа i1 конструктор по умолчанию для i1 вызывает base(int), как написано.

Но когда вы создаете объект типа d, конструктор для d отвечает за создание объекта base. Поскольку d не имеет конструктора по умолчанию, компилятор генерирует тот, который вызывает конструктор по умолчанию для base, прежде чем он вызовет конструкторы по умолчанию для i1 и i2.

int main() {
    d d_obj;
    return 0;
}

вывода здесь

[temp]$ Clang++ test.cpp
[temp]$ ./a.out
base()
i1()
i2()
[temp]$ 

Обратите внимание, что конструкторы для i1 и i2 не создали субобъект base. Компилятор позаботился о том, чтобы: база должна была быть инициализирована только один раз, и конструктор для d сделал это.

Если вы хотите другую инициализацию для базового объекта, напишите это в конструкторе:

d::d() : base(2) {}

добавление этого к классу d дает такой вывод:

[temp]$ Clang++ test.cpp
[temp]$ ./a.out
base(int)
i1()
i2()
[temp]$ 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...