Управление базовым конструктором в производных классах, потенциальная двойная инициализация
Относительно этого вопроса и ответа на него, похоже, есть исключение, но оно вызвало у меня больше вопросов, чем ответов на них. Учти это:
#include <iostream>
using namespace std;
struct base {
virtual void test() {cout << "base::test" << endl;}
base() {test();}
virtual ~base() {}
};
struct derived : base {
virtual void test() {cout << "derived::test" << endl;}
derived() : base() {}
~derived() {}
};
int main() {
derived d;
return 0;
}
Я наивно думал, что это напечатало бы только одно из двух сообщений. На самом деле он печатает оба - сначала базовую версию, а затем производную. Это ведет себя так же на -O0
а также -O3
настройки, так что это не оптимизация или ее отсутствие, насколько я могу судить.
Должен ли я понять это призвание base
(или конструкторы более высоких / более ранних классов) в пределах derived
конструктор, не помешает по умолчанию base
конструктор (или иным образом) от вызова заранее?
То есть последовательность в приведенном выше фрагменте при построении derived
Объект является: base()
затем derived()
и в этом base()
снова?
Я знаю, что не имеет смысла изменять vtable только для вызова base::base()
назад к тому, что было раньше derived::derived()
был вызван, просто ради вызова другого конструктора. Я могу только догадываться, что связанные с vtable вещи жестко запрограммированы в цепочке конструкторов, и вызов предыдущих конструкторов буквально интерпретируется как правильный вызов метода (вплоть до самого производного объекта, созданного в цепочке до сих пор)?
Помимо этих мелких вопросов, возникает два важных:
1. Всегда ли вызов базового конструктора в производном конструкторе будет вызывать вызов базового конструктора по умолчанию до вызова производного конструктора в первую очередь? Разве это не неэффективно?
2. Существует ли сценарий использования, в котором базовый конструктор по умолчанию, согласно #1, не должен использоваться вместо базового конструктора, явно вызываемого в конструкторе производных классов? Как это может быть достигнуто в C++?
Я знаю, что #2 звучит глупо, ведь у вас нет гарантии, что состояние части базового класса производного класса было "готово" / "построено", если бы вы могли отложить вызов базового конструктора до произвольного вызова функции в производном конструктор. Так, например, это:
derived::derived() { base::base(); }
... Я ожидал бы вести себя одинаково и вызывать базовый конструктор дважды. Однако есть ли причина, по которой компилятор, похоже, рассматривает это так же, как этот?
derived::derived() : base() { }
Я не уверен. Но это, кажется, эквивалентные утверждения, насколько наблюдаемые эффекты идут. Это противоречит идее, которую я имел в виду, что базовый конструктор может быть перенаправлен (по крайней мере, в некотором смысле) или, возможно, лучший выбор слова будет выбран в производном классе с использованием :base()
синтаксис. Действительно, эта запись требует, чтобы базовые классы были помещены перед членами, отличными от производного класса...
Другими словами, этот ответ и его пример (на мгновение забудем его C#) дважды вызовут базовый конструктор? Хотя я понимаю, почему он так делает, я не понимаю, почему он не ведет себя более "интуитивно", и выбираю базовый конструктор (по крайней мере, для простых случаев) и вызываю его только один раз.
Разве это не риск двойной инициализации объекта? Или это частичное предположение, что объект неинициализирован при написании кода конструктора? в худшем случае я должен теперь предположить, что каждый ученик потенциально может быть инициализирован дважды и защититься от этого?
Я закончу ужасным примером - но разве это не утечка памяти? следует ли ожидать утечки?
#include <iostream>
using namespace std;
struct base2 {
int * member;
base2() : member(new int) {}
base2(int*m) : member(m) {}
~base2() {if (member) delete member;}
};
struct derived2 : base2 {
derived2() : base2(new int) {
// is `member` leaking?
// should it be with this syntax?
}
};
int main() {
derived2 d;
return 0;
}
2 ответа
но разве это не утечка памяти? следует ли ожидать утечки?
нет. Последовательность операций будет:
derived2::derived2()
auto p = new int
base2::base2(p)
base2::member = p
И для деструктора:
derived2::~derived2() (implied)
base2::~base2()
if (base2::member) { delete base2::member; }
Один новый, один удалить. Отлично.
Не забудьте написать правильные конструкторы присваивания / копирования.
Для построения производного объекта класса компилятора необходимо построить его базовую часть. Вы можете указать, какой компилятор конструктора базового класса следует использовать, derived2() : base2(new int)
,
Если вам не хватает такой спецификации, компилятор будет использовать базовый конструктор по умолчанию.
Итак, базовый конструктор будет вызываться только один раз, и пока ваш код не вызывает утечку памяти, его не будет.