Виртуальные функции в конструкторах, почему языки различаются?

В 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, но они решили пойти другим путем, и, возможно, это единственная причина для принцип наименьшего удивления?

Как вы думаете, какой выбор правильный?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
12
0
3 752
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Оба пути могут привести к неожиданным результатам. Лучше всего вообще не вызывать виртуальную функцию в конструкторе.

Я думаю, что способ C++ имеет больше смысла, но приводит к проблемам с ожиданием, когда кто-то просматривает ваш код. Если вы знаете об этой ситуации, вам не следует намеренно помещать свой код в эту ситуацию для более поздней отладки.

Я думаю, что C++ предлагает лучшую семантику с точки зрения «наиболее правильного» поведения ... однако это больше работы для компилятора, и код определенно не интуитивно понятен для тех, кто читает его позже.

В подходе .NET функция должна быть очень ограничена, чтобы не полагаться на какое-либо состояние производного объекта.

Как это дополнительная работа для компилятора? Все сводится к установке vptr после вызова конструктора базового класса. Я бы сказал, что другую семантику сложнее реализовать, поскольку вам нужно убедиться, что после установки vptr в конструкторе производного класса он не должен быть переопределен конструкторами базовых классов. (Предполагается, что динамическая отправка обрабатывается с помощью указателей на таблицы виртуальных методов, что является наиболее распространенным подходом.)

Luc Touraille 25.05.2012 20:17

@LucTouraille очень запоздал с +1, это правда. подход C++ намного чище и прямолинейнее, на самом деле

Stephen Lin 03.03.2013 14:12

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++ вызывает деструкторы статических объектов при выходе, очень долго меня бесили.

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