В C++, когда виртуальная функция вызывается из конструктора, она не ведет себя как виртуальная функция.
Я думаю, что все, кто впервые столкнулся с таким поведением, были удивлены, но, если подумать, это имело смысл:
Пока производный конструктор не был выполнен, объект является нет еще экземпляром полученный.
Итак, как можно вызвать производную функцию? Предварительные условия не были созданы. Пример:
class base {
public:
base()
{
std::cout << "foo is " << foo() << std::endl;
}
virtual int foo() { return 42; }
};
class derived : public base {
int* ptr_;
public:
derived(int i) : ptr_(new int(i*i)) { }
// The following cannot be called before derived::derived due to how C++ behaves,
// if it was possible... Kaboom!
virtual int foo() { return *ptr_; }
};
То же самое для Java и .NET, но они решили пойти другим путем, и, возможно, это единственная причина для принцип наименьшего удивления?
Как вы думаете, какой выбор правильный?




Оба пути могут привести к неожиданным результатам. Лучше всего вообще не вызывать виртуальную функцию в конструкторе.
Я думаю, что способ C++ имеет больше смысла, но приводит к проблемам с ожиданием, когда кто-то просматривает ваш код. Если вы знаете об этой ситуации, вам не следует намеренно помещать свой код в эту ситуацию для более поздней отладки.
Я думаю, что C++ предлагает лучшую семантику с точки зрения «наиболее правильного» поведения ... однако это больше работы для компилятора, и код определенно не интуитивно понятен для тех, кто читает его позже.
В подходе .NET функция должна быть очень ограничена, чтобы не полагаться на какое-либо состояние производного объекта.
@LucTouraille очень запоздал с +1, это правда. подход C++ намного чище и прямолинейнее, на самом деле
Virtual functions in constructors, why do languages differ?
Потому что нет ни одного хорошего поведения. Я считаю, что поведение C++ имеет больше смысла (поскольку c-tor базового класса вызываются первыми, очевидно, что они должны вызывать виртуальные функции базового класса - в конце концов, c-tor производного класса еще не запущен, поэтому он возможно, не были установлены правильные предварительные условия для виртуальной функции производного класса).
Но иногда, когда я хочу использовать виртуальные функции для инициализации состояния (поэтому не имеет значения, что они вызываются с неинициализированным состоянием), поведение C# / Java лучше.
Delphi хорошо использует виртуальные конструкторы в среде графического интерфейса VCL:
type
TComponent = class
public
constructor Create(AOwner: TComponent); virtual; // virtual constructor
end;
TMyEdit = class(TComponent)
public
constructor Create(AOwner: TComponent); override; // override virtual constructor
end;
TMyButton = class(TComponent)
public
constructor Create(AOwner: TComponent); override; // override virtual constructor
end;
TComponentClass = class of TComponent;
function CreateAComponent(ComponentClass: TComponentClass; AOwner: TComponent): TComponent;
begin
Result := ComponentClass.Create(AOwner);
end;
var
MyEdit: TMyEdit;
MyButton: TMyButton;
begin
MyEdit := CreateAComponent(TMyEdit, Form) as TMyEdit;
MyButton := CreateAComponent(TMyButton, Form) as TMyButton;
end;
Существует фундаментальная разница в том, как языки определяют время жизни объекта. В Java и .Net члены объекта инициализируются нулем / нулем перед запуском любого конструктора, и именно в этот момент начинается время жизни объекта. Итак, когда вы входите в конструктор, у вас уже есть инициализированный объект.
В C++ время жизни объекта начинается только после завершения работы конструктора (хотя переменные-члены и базовые классы полностью создаются до его запуска). Это объясняет поведение при вызове виртуальных функций, а также то, почему деструктор не запускается, если в теле конструктора есть исключение.
Проблема с определением времени жизни объекта в Java / .Net заключается в том, что сложнее убедиться, что объект всегда соответствует своему инварианту, без необходимости указывать особые случаи, когда объект инициализирован, но конструктор не запущен. Проблема с определением C++ заключается в том, что у вас есть этот нечетный период, когда объект находится в подвешенном состоянии и не полностью построен.
Меня очень раздражает поведение C++. Вы не можете писать виртуальные функции, например, чтобы возвращать желаемый размер объекта и чтобы конструктор по умолчанию инициализировал каждый элемент. Например, неплохо было бы сделать:
BaseClass() {
for (int i=0; i<virtualSize(); i++)
initialize_stuff_for_index(i);
}
Опять же, преимущество поведения C++ заключается в том, что он препятствует написанию конструкторов, подобных приведенным выше.
Я не думаю, что проблема вызова методов, предполагающих, что конструктор завершен, является хорошим оправданием для C++. Если бы это действительно было проблемой, то конструктору не разрешалось бы вызывать методы любой, поскольку та же проблема может относиться к методам базового класса.
Еще один аргумент против C++ заключается в том, что его поведение намного менее эффективно. Хотя конструктор напрямую знает, что он вызывает, указатель vtab должен быть изменен для каждого отдельного класса с базового на окончательный, поскольку конструктор может вызывать другие методы, которые будут вызывать виртуальные функции. По моему опыту, это тратит гораздо больше времени, чем можно сэкономить, делая вызовы виртуальных функций в конструкторе более эффективными.
Гораздо более досадно то, что это верно и для деструкторов. Если вы напишете виртуальную функцию cleanup (), а деструктор базового класса выполняет cleanup (), он определенно не сделает то, что вы ожидаете.
Это, а также тот факт, что C++ вызывает деструкторы статических объектов при выходе, очень долго меня бесили.
Как это дополнительная работа для компилятора? Все сводится к установке vptr после вызова конструктора базового класса. Я бы сказал, что другую семантику сложнее реализовать, поскольку вам нужно убедиться, что после установки vptr в конструкторе производного класса он не должен быть переопределен конструкторами базовых классов. (Предполагается, что динамическая отправка обрабатывается с помощью указателей на таблицы виртуальных методов, что является наиболее распространенным подходом.)