Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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;

Ответ 6

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

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

В то же время преимущество поведения С++ заключается в том, что оно препятствует написанию кондукторов, как указано выше.

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

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

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

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