LINUX.ORG.RU
ФорумTalks

[C++] [Жаба] Это магия?

 


0

0

Сначала программа на 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()
★★★

При описывании функции в C++ разве надо писать virtual? В смысле уже в самой реализации.

//Сейчас точно уже не помню

Deleted
()

В джаве конструкторы вызываются после полного конструирования объекта и виртуальность в них работает нормально.

Legioner ★★★★★
()
Ответ на: комментарий от Legioner

Кстати, не понимаю, почему это вызывает удивление. У меня вызывало удивление поведение С++-а.

Legioner ★★★★★
()
Ответ на: комментарий от anonymous

В смысле, vtable pointer указывает на правильную виртуальную таблицу. В общем работает виртуальность в конструкторах (только надо понимать, что конструктор потомка ещё не вызывался и поля не заполнены).

Legioner ★★★★★
()
Ответ на: комментарий от Sikon

>При перекрытии виртуальной функции суперкласса - не надо. Но можно написать явно.

Очень желательно писать всегда, чтобы желающему расширить класс не надо было шерстить всю иерархию.

Absurd ★★★
()
Ответ на: комментарий от Legioner

Вот!

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

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

А вот поведение Жабы явилось здесь для меня неочевидным. По-видимому, инициализация vptr здесь осуществляется внутри JVM, между выделением памяти под объект и вызовом конструктора. Это, с одной стороны, перекладывает часть "магических" действий с конструктора на код, который его вызывает (это можно сделать, поскольку все объекты всё равно создаются с помощью new), но с другой стороны, делает конструкторы базового класса зависимыми от потенциальных переопределений вызываемых в нём функций, а значит, менее предсказуемыми.

Sikon ★★★
() автор топика
Ответ на: комментарий от Sikon

> делает конструкторы базового класса зависимыми от потенциальных переопределений вызываемых в нём функций, а значит, менее предсказуемыми.

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

Legioner ★★★★★
()
Ответ на: комментарий от Legioner

С идеологической точки зрения, когда выполняется конструктор базового класса, объект ещё не является объектом производного класса. В частности, если вызываемые из конструктора функции зависят от ещё не инициализированных переменных, получается чёрти что.

Аналогично с деструкторами, но в Жабе деструкторов нет.

Sikon ★★★
() автор топика
Ответ на: комментарий от Absurd

>> При перекрытии виртуальной функции суперкласса - не надо. Но можно написать явно.

> Очень желательно писать всегда, чтобы желающему расширить класс не надо было шерстить всю иерархию.

К сведению стунентегов и школьнегов:

Вариант А:

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.

anonymous
()
Ответ на: комментарий от anonymous

А бинарники при этом будут отличаться?

::raited

anonymous
()
Ответ на: комментарий от Legioner

> Что такое роутинг интерфейса?

Это не есть точный технический термин, но я думал что меня поймут.

Роутинг интерфейса - точнее говоря роутинг вызова метода для указателя на класс типа X, - всего-навсего выборка адреса из таблицы вирт-функций и вызов нужного метода.

Интерфейс в данном случае - набор методов, хранимых в виртуальной таблице. Каждая декларация class ... {...}, если среди нее есть методы с ключевым словом virtual, порождает свою таблицу виртуальных функций. В указанном мной примере в обьекте данные будут расположены следующим образом:

--------- | Base_VPTR --------- | Base_Data --------- | Impl_VPTR --------- | Impl_Data ---------

anonymous
()
Ответ на: комментарий от Legioner

Черт, форматтинг.

> Что такое роутинг интерфейса?

Это не есть точный технический термин, но я думал что меня поймут.

Роутинг интерфейса - точнее говоря роутинг вызова метода для указателя на класс типа X, - всего-навсего выборка адреса из таблицы вирт-функций и вызов нужного метода.

Интерфейс в данном случае - набор методов, хранимых в виртуальной таблице. Каждая декларация class ... {...}, если среди нее есть методы с ключевым словом virtual, порождает свою таблицу виртуальных функций. В указанном мной примере в обьекте данные будут расположены следующим образом:


---------
| Base_VPTR
---------
| Base_Data
---------
| Impl_VPTR
---------
| Impl_Data
---------

P.S. Да, бинарники будут отличатся, в случае А имеем:


---------
| Base_VPTR
---------
| Base_Data
---------
| Impl_Data
---------

anonymous
()
Ответ на: комментарий от anonymous

Т.е. вы утверждаете, что sizeof(Derived) будет равен 2 * sizeof(void*) ? Это меня весьма удивляет, и я обязательно проверю. Всю жизнь думал, что для каждого класса создаётся своя vtable и в любом объекте не более одного указателя на vtable.

Legioner ★★★★★
()
Ответ на: комментарий от anonymous

>К сведению стунентегов и школьнегов:

Данный пример доказывает что С++ еще большее говно чем я говорил о нем ранее.

Absurd ★★★
()
Ответ на: комментарий от Legioner

> Т.е. вы утверждаете, что sizeof(Derived) будет равен 2 * sizeof(void*) ?

Возможно в примитивных случаях компилятор как-то все и оптимизирует.

> Это меня весьма удивляет, и я обязательно проверю. Всю жизнь думал, что для каждого класса создаётся своя vtable и в любом объекте не более одного указателя на vtable.

Никак нет, хотя-бы из-за множественного наследования. Представьте класс X, и класс Y, каждый имеет свой vtable. А теперь на сцену выходит класс Z, наследующий оба класса X и Y, используя множественное наследование. Очевидно, что-бы осталась бинарная совместимость при вызове через указатели на базовые классы X и Y, нужно иметь в точности такое-же бинарное представление, как и у просто X и Y. Отсюда - как минимум 2 vtable в подобном случае.

anonymous
()
Ответ на: комментарий от anonymous

хех, а про собственно первый свой пример с вариантами А и Б - я похоже нагнал, но это не отменяет множественного наследования.

anonymous
()
Ответ на: комментарий от anonymous

>> Т.е. вы утверждаете, что sizeof(Derived) будет равен 2 * sizeof(void*) ?

> Возможно в примитивных случаях компилятор как-то все и оптимизирует.

А можно пример кода, который не оптимизируется GCC 4.x?

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

> А можно пример кода, который не оптимизируется GCC 4.x?

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

Для множественного наследования, более хитростный пример:

class B1
{
public:
virtual bool a() {} // gcc сожрет
}

class B2 : public B1
{
public:
virtual bool a() {} // gcc сожрет
}

class C : public B1, B2
{
public:
virtual bool a() {} // gcc сожрет
}

sizeof(C) - будет 8.

anonymous
()
Ответ на: комментарий от anonymous

>> А можно пример кода, который не оптимизируется GCC 4.x?

> Не, я уже поправился, множественные vtable-ы появляются только при множественном наследовании

Когда я начал писать вопрос, поправки еще не было %)

tailgunner ★★★★★
()
Ответ на: комментарий от Absurd

>Данный пример доказывает что С++ еще большее говно чем я говорил о нем ранее.

Данный пост показывает, что ты знаешь С++ ещё хуже, чем я думал ранее, раз для тебя изложенное в этом примере было открытием %)

MYMUR ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.