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

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

Какой из них вы считаете правильным?

6 ответов

Решение

Существует фундаментальное различие в том, как языки определяют время жизни объекта. В Java и.Net члены объекта инициализируются нулями / нулями до запуска любого конструктора, и в этот момент начинается время жизни объекта. Поэтому, когда вы входите в конструктор, вы уже получили инициализированный объект.

В C++ время жизни объекта начинается только тогда, когда заканчивается конструктор (хотя переменные-члены и базовые классы полностью создаются до его запуска). Это объясняет поведение при вызове виртуальных функций, а также почему деструктор не запускается, если в теле конструктора есть исключение.

Проблема с определением времени жизни объекта в Java/.Net состоит в том, что труднее убедиться, что объект всегда соответствует инварианту, без необходимости помещать его в особые случаи, когда объект инициализируется, но конструктор не запускается. Проблема с определением C++ состоит в том, что у вас есть этот странный период, когда объект находится в подвешенном состоянии и не полностью построен.

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

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

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

Потому что нет ни одного хорошего поведения. Я считаю, что поведение C++ имеет больше смысла (поскольку c-tors базового класса вызывается первым, вполне понятно, что они должны вызывать виртуальные функции базового класса - в конце концов, производный класс c-tor еще не запущен, поэтому может не установить правильные предварительные условия для виртуальной функции производного класса).

Но иногда, когда я хочу использовать виртуальные функции для инициализации состояния (поэтому не имеет значения, что они вызываются с неинициализированным состоянием), поведение C#/Java лучше.

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

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

Я нахожу поведение C++ очень раздражающим. Вы не можете написать виртуальные функции, например, чтобы вернуть желаемый размер объекта, и чтобы конструктор по умолчанию инициализировал каждый элемент. Например, было бы неплохо сделать:

BaseClass() { for (int i=0; i<virtualSize(); i++) initialize_stuff_for_index(i); }

С другой стороны, преимущество поведения C++ состоит в том, что оно препятствует написанию таких конструкторов, как описанные выше.

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

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

Гораздо более раздражающим является то, что это относится и к деструкторам. Если вы пишете виртуальную функцию cleanup(), а деструктор базового класса выполняет cleanup(), он, безусловно, не выполняет то, что вы ожидаете.

Это и тот факт, что C++ вызывает деструкторы на статических объектах при выходе, очень долго меня бесили.

Delphi хорошо использует виртуальные конструкторы в среде VCL GUI:

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;
Другие вопросы по тегам