Управление базовым конструктором в производных классах, потенциальная двойная инициализация

Относительно этого вопроса и ответа на него, похоже, есть исключение, но оно вызвало у меня больше вопросов, чем ответов на них. Учти это:

#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),

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

Итак, базовый конструктор будет вызываться только один раз, и пока ваш код не вызывает утечку памяти, его не будет.

Другие вопросы по тегам