Сначала программа на C++:
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor" << std::endl;
nonvirt();
}
void nonvirt() {
virt();
}
virtual void virt() = 0;
};
class Derived: public Base {
public:
virtual void virt() {
std::cout << "Derived.virt()" << std::endl;
}
};
int main() {
Derived d;
}
Результат:
Base constructor
pure virtual method called
terminate called without an active exception
Aborted
Теперь программа на Жабе:
import static java.lang.System.out;
public class Test {
public static abstract class Base {
public Base() {
out.println("Base constructor");
virt();
}
public abstract void virt();
}
public static class Derived extends Base {
@Override
public void virt() {
out.println("Derived.virt()");
}
}
public static void main(String[] args) {
new Derived();
}
}
Результат:
Base constructor
Derived.virt()
В смысле, vtable pointer указывает на правильную виртуальную таблицу. В общем работает виртуальность в конструкторах (только надо понимать, что конструктор потомка ещё не вызывался и поля не заполнены).
Поведение C++ тут как раз понятно. Первым действием конструктора является вызов конструктора суперкласса, а затем инициализация vptr. Поэтому, когда выполняется конструктор суперкласса, его vptr указывает на vtable базового класса, поэтому никакие переопределённые функции не вызываются. Особо вумные компиляторы (вроде редмондского) могут даже превратить вызов в невиртуальный, если не оборачивать вызов виртуальной функции в вызов невиртуальной (как в примере) - результатом будет ошибка времени линковки, а не выполнения.
Сделано это, как я понимаю, из соображений простоты реализации, а в деструкторах сделали симметрично (первым шагом деструктора опять-таки является присваивание указателя на vtable именно этого класса) из подведённых под это поведение идеологических соображений.
А вот поведение Жабы явилось здесь для меня неочевидным. По-видимому, инициализация vptr здесь осуществляется внутри JVM, между выделением памяти под объект и вызовом конструктора. Это, с одной стороны, перекладывает часть "магических" действий с конструктора на код, который его вызывает (это можно сделать, поскольку все объекты всё равно создаются с помощью new), но с другой стороны, делает конструкторы базового класса зависимыми от потенциальных переопределений вызываемых в нём функций, а значит, менее предсказуемыми.
> делает конструкторы базового класса зависимыми от потенциальных переопределений вызываемых в нём функций, а значит, менее предсказуемыми.
Любая функция, вызывающая виртуальную функцию, зависит от потенциальных переопределений. Переопределяют функции не враги народа а нормальные люди, и если они это делают, значит это кому то надо.
С идеологической точки зрения, когда выполняется конструктор базового класса, объект ещё не является объектом производного класса. В частности, если вызываемые из конструктора функции зависят от ещё не инициализированных переменных, получается чёрти что.
Аналогично с деструкторами, но в Жабе деструкторов нет.
>> При перекрытии виртуальной функции суперкласса - не надо. Но можно написать явно.
> Очень желательно писать всегда, чтобы желающему расширить класс не надо было шерстить всю иерархию.
К сведению стунентегов и школьнегов:
Вариант А:
class Base
{
public:
virtual bool doSomething() = 0;
};
class Impl : public Base
{
...
bool doSomething(); // а реализация - в *.cpp
}
и,
Вариант Б:
class Base
{
public:
virtual bool doSomething() = 0;
};
class Impl : public Base
{
...
virtual bool doSomething(); // а реализация - в *.cpp
}
породят __РАЗНЫЕ__ классы Impl: в случае Б, класса Impl будет 2 таблицы виртуальных функций, - одна для роутинга интерфейса описанного в Base, и другая - для роутинга интерфейса описанного в Impl.
Это не есть точный технический термин, но я думал что меня поймут.
Роутинг интерфейса - точнее говоря роутинг вызова метода для указателя на класс типа X, - всего-навсего выборка адреса из таблицы вирт-функций и вызов нужного метода.
Интерфейс в данном случае - набор методов, хранимых в виртуальной таблице. Каждая декларация class ... {...}, если среди нее есть методы с ключевым словом virtual, порождает свою таблицу виртуальных функций. В указанном мной примере в обьекте данные будут расположены следующим образом:
Это не есть точный технический термин, но я думал что меня поймут.
Роутинг интерфейса - точнее говоря роутинг вызова метода для указателя на класс типа X, - всего-навсего выборка адреса из таблицы вирт-функций и вызов нужного метода.
Интерфейс в данном случае - набор методов, хранимых в виртуальной таблице. Каждая декларация class ... {...}, если среди нее есть методы с ключевым словом virtual, порождает свою таблицу виртуальных функций. В указанном мной примере в обьекте данные будут расположены следующим образом:
Т.е. вы утверждаете, что sizeof(Derived) будет равен 2 * sizeof(void*) ? Это меня весьма удивляет, и я обязательно проверю. Всю жизнь думал, что для каждого класса создаётся своя vtable и в любом объекте не более одного указателя на vtable.
> Т.е. вы утверждаете, что sizeof(Derived) будет равен 2 * sizeof(void*) ?
Возможно в примитивных случаях компилятор как-то все и оптимизирует.
> Это меня весьма удивляет, и я обязательно проверю. Всю жизнь думал, что для каждого класса создаётся своя vtable и в любом объекте не более одного указателя на vtable.
Никак нет, хотя-бы из-за множественного наследования. Представьте класс X, и класс Y, каждый имеет свой vtable. А теперь на сцену выходит класс Z, наследующий оба класса X и Y, используя множественное наследование. Очевидно, что-бы осталась бинарная совместимость при вызове через указатели на базовые классы X и Y, нужно иметь в точности такое-же бинарное представление, как и у просто X и Y. Отсюда - как минимум 2 vtable в подобном случае.