Вызов виртуального метода в конструкторе: разница между 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()

Однако в 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 инициализируется?

РЕДАКТИРОВАТЬ: Я понимаю механизмы Java и C ++. Что я хочу знать, так это понимание этого дизайнерского решения.

15

Решение

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

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

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

14

Другие решения

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

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

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

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

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

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

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

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

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

Для более полного рассмотрения наиболее распространенного случая, см. Также мою статью в блоге «Как избежать пост-строительства, используя фабрики запчастей».

11

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

7

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

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

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

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

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

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

2

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

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

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

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

0

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

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

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

0

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

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

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

Добавлено 21 декабря 2016


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

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

0
По вопросам рекламы [email protected]