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

В 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()

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

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

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

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

EDIT: Я понимаю механизмы Java и С++. То, что я хочу знать, - это понимание этого дизайнерского решения.

Ответ 1

Оба подхода явно имеют недостатки:

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

Почему каждый язык делает то, что он делает, является открытым вопросом, но оба, вероятно, утверждают, что это "безопасный" вариант: способ С++ предотвращает использование неинициализированных членов; Подход Javas позволяет полиморфную семантику (в некоторой степени) внутри конструктора класса (который является вполне допустимым прецедентом).

Ответ 2

Ну, вы уже связались с обсуждение вопросов, но это в основном проблемно-ориентированное, не вникающее в обоснование, почему.

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

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

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

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

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

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

Тем не менее, немного сложно выполнить инициализацию производного класса в конструкторе базового класса, например. делать общие вещи в самом верхнем GUI Widget классе & rsquo; конструктор.

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

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

Ответ 3

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

Если понимать виртуальную концепцию четко, эквивалент в java является абстрактным, ваш код прямо сейчас не является действительно виртуальным кодом в java-терминах.

Счастлив обновить мой ответ, если что-то не так.

Ответ 6

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

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

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

Ответ 7

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

  • Либо строго соответствуют семантике на неполиморфных конструктор и, следовательно, рассмотрим любой полиморфный метод, вызываемый внутри конструктор как неполиморфный. Вот как С++ делает §.
  • Или, компромисс строгая семантика неполиморфного конструктора и придерживаться строгая семантика полиморфного метода. Таким образом, полиморфные методы из конструкторов всегда полиморфны. Так работает Java.

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

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


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

Если класс C имеет прямое определение некоторой виртуальной функции F, и ее ctor имеет вызов в F, то любое (косвенное) обращение C s ctor на экземпляр дочернего класса T не будет влияют на выбор F; и на самом деле C::F всегда будет вызываться из C s ctor. В этом смысле вызов виртуального F является менее полиморфным (по сравнению, например, Java, который выберет F на основе T)
Кроме того, важно отметить, что если C наследует определение F от некоторого родителя P и не переопределяет F, тогда C s ctor будет вызывать P::F и даже это, IMHO, может быть определено статически.