Вызов виртуального метода в конструкторе: разница между Java и C++

В Java:

class Base {
    public Base() { System.out.println("Base::Base()"); virt(); }
    void virt()   { System.out.println("Base::virt()"); }
}

class Derived extends Base {
    public Derived() { System.out.println("Derived::Derived()"); virt(); }
    void virt()      { System.out.println("Derived::virt()"); }
}

public class Main {
    public static void main(String[] args) {
        new Derived();
    }
}

выводится

Base::Base()
Derived::virt()
Derived::Derived()
Derived::virt()

однако в C++ результат отличается:

Base::Base()
Base::virt() // ← Not Derived::virt()
Derived::Derived()
Derived::virt()

(см. http://www.parashift.com/c++-faq-lite/calling-virtuals-from-ctors.html для кода C++)

что вызывает такую разницу между Java и c++? Это время, когда vtable инициализируется?

EDIT: я понимаю механизмы Java и c++. Что я хочу знать выводы за этим проектным решением.

7 ответов


оба подхода явно имеют неудачи:

  • в Java вызов переходит к методу, который не может использовать this правильно, потому что его члены еще не были инициализированы.
  • в C++ вызывается неинтуитивный метод (т. е. не тот, что в производном классе), если вы не знаете, как C++ создает классы.

почему каждый язык делает то, что он делает, - это открытый вопрос, но оба, вероятно, утверждают, что это "безопасный" вариант: C++ способ позволяет использовать uninitialsed членов; подход в Java позволяет полиморфных семантики (до некоторой степени) внутри класса’ конструктор (что вполне допустимо использовать-дело).


Ну, вы уже связаны с "вопросы и ответы"!--5-->, но это в основном ориентировано на проблему, не вдаваясь в объяснения,почему.

короче говоря, это для безопасность типа.

Это один из немногих случаев, когда C++ бьет Java и C# по безопасности типов. ;-)

при создании класса A в C++ вы можете позволить каждому A конструктор инициализирует новый экземпляр так, чтобы все общие предположения о его государство, называемое инвариантный класс удерживайте. Например, частью инварианта класса может быть то, что элемент указателя указывает на некоторую динамически выделенную память. Когда каждый общедоступный метод сохраняет инвариант класс, тогда он гарантированно удерживается также при входе в каждый метод, что значительно упрощает вещи – по крайней мере, для хорошо выбранного инварианта класса!

дальнейшая проверка не требуется в каждом методе.

напротив, используя двухфазная инициализация, например, в библиотеках Microsoft MFC и ATL, вы никогда не можете быть уверены, что все было правильно инициализировано при вызове метода (нестатической функции-члена). Это очень похоже на Java и C#, за исключением того, что в этих языках отсутствие инвариантных гарантий класса исходит из этих языков, просто позволяя, но не активно поддерживая концепцию инварианта класса. Короче говоря, виртуальные методы Java и C#, вызываемые из конструктора базового класса, могут быть вызывается на производном экземпляре, который еще не инициализирован, где инвариант (производного) класса еще не установлен!

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

однако, это делает немного трудно сделать инициализация производного класса в конструкторе базового класса, например, делать общие вещи в самом верхнем Графический интерфейс класса.

пункт FAQ "хорошо, но есть ли способ имитировать это поведение, как если бы динамическая привязка работала над этим объектом в конструкторе моего базового класса?" немного вдается в это.

для более полного рассмотрения наиболее распространенного случая см. Также мою статью в блоге "как избежать пост-конструкции путем использование фабрик частей".


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


надеюсь, это поможет:

когда ваша строка new Derived() выполняется, первое, что происходит, это выделение памяти. Программа выделит кусок памяти, достаточно большой, чтобы вместить оба члена Base и Derrived. В этот момент нет объекта. Это просто неинициализированная память.

, когда Baseконструктор завершен, память будет содержать объект типа Base, и инвариант класса для Base должны держать. По-прежнему нет Derived объект в памяти.

во время строительство базы,Base объект находится в частично построенном состоянии, но правила языка доверяют вам достаточно, чтобы вы могли вызывать свои собственные функции-члены на частично построенном объекте. The Derived объект не частично построен. Его не существует.

ваш вызов виртуальной функции заканчивается вызовом версии базового класса, потому что в этот момент времени, Base является наиболее производным типом объекта. если бы он позвонил Derived::virt, это будет вызов функции-члена Derived с указателем this, который не имеет типа Derrived, нарушая безопасность типов.

логически, класс-это то, что строится, имеет вызываемые на нем функции, а затем уничтожается. Нельзя вызывать функции-члены для объекта, который не был создан, и нельзя вызывать функции-члены для объекта после его уничтожения. Это довольно фундаментально для ООП, правила языка C++ просто помогают вам избежать действий, которые нарушают эту модель.


в Java вызов метода основан на типе объекта, поэтому он ведет себя так (я мало знаю о c++).

здесь ваш объект имеет тип Derived, поэтому jvm вызывает метод на


на самом деле я хочу знать, каково понимание этого дизайнерского решения

возможно, что в Java каждый тип происходит от Object, каждый объект является каким-то типом листа, и есть одна JVM, в которой все объекты построены.

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


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

  • либо строго соответствовать семантике на полиморфные конструктор и, таким образом, рассмотрим любой полиморфный метод, вызываемый в a конструктор как неполиморфный. Это как C++ делает§.
  • или компромисс строгая семантика non-polymorphic конструктора и придерживается к строгая семантика полиморфного метода. Таким образом, полиморфные методы из конструкторов всегда полиморфны. Вот как работает Java.

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

добавлено 21-Dec-2016


§чтобы заявление "метод, вызываемый в конструкторе как неполиморфный...Вот как C++ делает" может быть запутанным, без тщательного изучения контекста, я добавляю формализация, чтобы точно определить, что я имел в виду.

Если класс C имеет прямое определение некоторой виртуальной функции F и его ctor имеет вызов F, затем любой (косвенный) вызов Cctor на экземпляре дочернего класса T не повлияет на выбор F; и в самом деле, C::F всегда будет вызываться из C’s ctor. В этом смысле вызов virtual F является менее полиморфным (по сравнению, скажем, Java, который выберет F на основе T)
Далее, важно отметить, что, если C наследует определение F от какого-то родителя P и не является overriden F, потом Cctor вызовет P::F и даже это, ИМХО, можно определить статически.