Вызов переопределенного метода из родительского класса ctor

Я попытался вызвать переопределенный метод из конструктора родительского класса и заметил различное поведение на разных языках.

C++ - эхо A.foo()

class A{

public: 

    A(){foo();}

    virtual void foo(){cout<<"A.foo()";}
};

class B : public A{

public:

    B(){}

    void foo(){cout<<"B.foo()";}
};

int main(){

    B *b = new B(); 
}

Java - эхо-сигналы B.foo()

class A{

    public A(){foo();}

    public void foo(){System.out.println("A.foo()");}
}

class B extends A{  

    public void foo(){System.out.println("B.foo()");}
}

class Demo{

    public static void main(String args[]){
        B b = new B();
    }
}

C# - эхо-сигналы B.foo()

class A{

    public A(){foo();}

    public virtual void foo(){Console.WriteLine("A.foo()");}
}

class B : A{    

    public override void foo(){Console.WriteLine("B.foo()");}
}


class MainClass
{
    public static void Main (string[] args)
    {
        B b = new B();              
    }
}

Я понимаю, что в С++ объекты создаются из самого верхнего родителя, идя вниз по иерархии, поэтому, когда конструктор вызывает переопределенный метод, B даже не существует, поэтому он вызывает версию A 'метода. Однако я не уверен, почему у меня другое поведение в Java и С# (из С++)

Ответ 1

В С++, как вы правильно отметили, объект имеет тип A, пока конструктор A не будет завершен. Объект фактически меняет тип во время его построения. Вот почему используется vtable класса A, поэтому A::foo() вызывается вместо B::foo().

В Java и С# виртуальная таблица (или эквивалентный механизм) самого производного типа используется повсюду даже при построении базовых классов. Поэтому на этих языках вызывается B.foo().

Обратите внимание, что обычно не рекомендуется вызывать виртуальный метод из конструктора. Если вы не очень осторожны, виртуальный метод может предположить, что объект полностью сконструирован, хотя это не так. В Java, где каждый метод неявно виртуальный, у вас нет выбора.

Ответ 2

Хотя я понимаю, что вы делаете это для экспериментов, важно отметить следующую цитату из Effective Java 2nd Edition, пункт 17: "Дизайн и документ для наследования", либо запретить это:

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

Вот пример, иллюстрирующий:

public class ConstructorCallsOverride {
    public static void main(String[] args) {
        abstract class Base {
            Base() { overrideMe(); }
            abstract void overrideMe(); 
        }
        class Child extends Base {
            final int x;
            Child(int x) { this.x = x; }
            @Override void overrideMe() {
                System.out.println(x);
            }
        }
        new Child(42); // prints "0"
    }
}

Здесь, когда конструктор Base вызывает overrideMe, Child не завершил инициализацию final int x, и метод получил неправильное значение. Это почти наверняка приведет к ошибкам и ошибкам.