Вызов конструктора базового класса из тела конструктора подкласса
У меня сложилось впечатление, что это невозможно, см., Например: вызов конструктора базового класса после некоторых других инструкций в C++
Но следующая программа запускается и выдает две строки "Конструктор":
#include <iostream>
class Person
{
public:
Person()
{
std::cout << "Constructor Person" << std::endl; }
};
class Child : public Person
{
public:
Child()
{
c = 1;
Person();
}
int c;
};
int main()
{
Child child;
return 0;
}
Первый - неявный вызов конструктора по умолчанию, это понятно. Как насчет второго - означает ли это, что действие, описанное в названии, является законным? Я использую Visual C++ 2010.
5 ответов
Ниже приводится выдержка из "Ускоренного C++":
"Производные объекты создаются:
1. Выделение пространства для всего объекта (членов базового класса, а также членов производного класса);
2. Вызов конструктора базового класса для инициализации части объекта базового класса;
3. Инициализация членов производного класса в соответствии с указаниями инициализатора конструктора;
4. Выполнение тела конструктора производного класса, если есть."
Обобщение ответов и комментариев: Вызов конструктора базового класса из тела конструктора подкласса невозможен в том смысле, что #2 выше должен предшествовать #4. Но мы все еще можем создать базовый объект в производном теле конструктора, таким образом вызывая базовый конструктор. Это будет объект, отличный от объекта, который создается с помощью выполняемого в данный момент производного конструктора.
Вызов внутри конструктора дочернего класса не вызывает конструктор базового класса, он создает временный, безымянный и новый объект типа Person. Он будет уничтожен при выходе из конструктора. Чтобы уточнить, ваш пример такой же, как и это:
Child() { c = 1; Person tempPerson; }
За исключением этого случая, временный объект имеет имя.
Вы можете понять, что я имею в виду, если немного измените свой пример:
class Person
{
public:
Person(int id):id(id) { std::cout << "Constructor Person " << id << std::endl; }
~Person(){ std::cout << "Destroying Person " << id << std::endl; }
int id;
};
class Child : public Person
{
public:
Child():Person(1) { c = 1; Person(2); }
int c;
};
int main() {
Child child;
Person(3);
return 0;
}
Это производит вывод:
Constructor Person 1
Constructor Person 2
Destroying Person 2
Constructor Person 3
Destroying Person 3
Destroying Person 1
Вы не можете вызвать его из тела дочернего конструктора, но вы можете поместить его в список инициализатора:
public:
Child() : Person() { c = 1; }
Конечно, не полезно вызывать конструктор по умолчанию для родительского элемента, потому что это произойдет автоматически. Это более полезно, если вам нужно передать параметр конструктору.
Причина, по которой вы не можете вызвать конструктор из тела, состоит в том, что C++ гарантирует, что родительский объект будет завершен до того, как начнется дочерний конструктор.
Ответы на этот вопрос, хотя обычно технически верны и полезны, не дают общей картины. И в целом картина несколько иная, чем может показаться:)
Конструктор базового класса вызывается всегда, иначе в теле конструктора производного класса будет частично сконструированный и, следовательно, непригодный для использования объект. У вас есть возможность предоставить аргументы конструктору базового класса. Это не "вызывает" его: оно вызывается несмотря ни на что, вы можете просто передать ему несколько дополнительных аргументов:
// Correct but useless the BaseClass constructor is invoked anyway DerivedClass::DerivedClass() : BaseClass() { ... } // A way of giving arguments to the BaseClass constructor DerivedClass::DerivedClass() : BaseClass(42) { ... }
Синтаксис C++ для явного вызова конструктора имеет странное имя и соответствует этому имени, потому что это то, что делается очень редко - обычно только в коде библиотеки / фундамента. Это называется новым размещением, и нет, это не имеет ничего общего с выделением памяти - это странный синтаксис для явного вызова конструкторов в C++:
// This code will compile but has undefined behavior // Do NOT do this // This is not a valid C++ program even though the compiler accepts it! DerivedClass::DerivedClass() { new (this) BaseClass(); /* WRONG */ } DerivedClass::DerivedClass() { new (this) BaseClass(42); /* WRONG */ } // The above is how constructor calls are actually written in C++.
Итак, в вашем вопросе это то, о чем вы хотели спросить, но не знали:) Я полагаю, что этот странный синтаксис полезен, поскольку, если бы это было легко, то люди пришли из языков, где такие вызовы конструкторов являются обычным явлением (например, Pascal/Delphi) мог написать множество, казалось бы, работающего кода, который будет полностью сломан всеми способами. Неопределенное поведение не является гарантией сбоя, в этом проблема. Поверхностный / очевидный UB часто приводит к сбоям (например, доступ к нулевому указателю), но большая часть UB - тихий убийца. Так что усложнение написания неправильного кода из-за неясного синтаксиса является желательной чертой языка.
"Второй вариант" в вопросе не имеет ничего общего с "вызовами" конструкторов. Синтаксис C++ создания созданного по умолчанию экземпляра значения
BaseClass
объект:// Constructs a temporary instance of the object, and promptly // destructs it. It's useless. BaseClass(); // Here's how the compiler can interpret the above code. You can write either // one and it has identical effects. Notice how the scope of the value ends // and you have no access to it. { BaseClass __temporary{}; }
В C++ понятие создания экземпляра объекта является всеобъемлющим: вы делаете это все время, поскольку семантика языка приравнивает существование объекта к тому, что объект был создан. Так что вы также можете написать:
// Constructs a temporary integer, and promptly destructs it. int();
Объекты целочисленного типа также создаются и разрушаются, но конструктор и деструктор тривиальны и, следовательно, не требуют дополнительных затрат.
Обратите внимание, что создание и уничтожение объекта таким образом не подразумевает выделения кучи. Если компилятор решает, что экземпляр должен быть фактически материализован (например, из-за наблюдаемых побочных эффектов построения или разрушения), экземпляр является временным объектом, точно так же, как временные объекты, созданные во время оценки выражения - а-ха, мы замечаем, что
type()
это выражение!Итак, в вашем случае это
Person();
заявление было запретным. В коде, скомпилированном в режиме выпуска, для него не создаются машинные инструкции, потому что нет возможности наблюдать эффекты этого оператора (в случае конкретногоPerson
class), и, таким образом, если никто не слышит, как дерево падает, то дерево вообще не обязательно должно существовать. Именно так компиляторы C++ оптимизируют материал: они проделывают много работы, чтобы доказать (формально, в математическом смысле), могут ли эффекты любого фрагмента кода быть ненаблюдаемыми, и если да, то код рассматривается как мертвый и удаляется.
Да, я знаю, что этому год, но я нашел способ сделать это. Возможно, это не лучшая практика. Например, уничтожение экземпляра базового класса из конструктора производного класса звучит как рецепт катастрофы. Вы можете пропустить этап деструктора, но это может привести к утечке памяти, если конструктор базового класса выполняет какое-либо выделение.
class Derived : public Base
{
public:
Derived()
{
// By the time we arrive here, the base class is instantiated plus
// enough memory has been allocated for the additional derived class stuff.
// You can initialize derived class stuff here
this->Base::~Base(); // destroy the base class
new (this) Base(); // overwrites the base class storage with a new instance
}
};