Вызов виртуальных методов в конструкторах

Модераторы: Hawk, Romeo, Absurd, DeeJayC, WinMain

BBB
Сообщения: 1298
Зарегистрирован: 27 дек 2005, 13:37

--------------------------------------------------------------
From Rycharg:
--------------------------------------------------------------
в конструкторах нельзя вызывать виртуальные функции
Позвольте не согласится с Вами, Romeo.

Код: Выделить всё

class foo{
   public:
   virtual void blabla(){ cout << "bla"; }
   foo(){}
};

class foo2 : public foo{
   public:
   virtual void blabla(){ cout << "blablablabla"; }
   foo2() { this->foo::blabla(); }
};

--------------------------------------------------------------
From Romeo:
--------------------------------------------------------------

Код: Выделить всё

foo2() { this->foo::blabla(); }
Абсолютно согласен с претензиями. Я не ошибся, а высказался неверно. Я имел ввиду, что в конструкторах не работает механизм виртуальности. Это как раз то, что мешает нам написать такой код:

Код: Выделить всё

class A
{
public:
   A(int i) : m_i(0) { i = GetI(); }

   virtual int GetI() = 0;
protected:
   int m_i;

};

class B : public A
{
public:
   virtual int GetI() { return SpecificForBValue(); }
   ...
};
Из кода видно, что подобный виртуальный вызов (будь он возможен), разрешил бы наши проблемы с классами OneRoom и его наследниками.

--------------------------------------------------------------
From BBB:
--------------------------------------------------------------
Romeo писал(а):

Код: Выделить всё

foo2() { this->foo::blabla(); }
Абсолютно согласен с претензиями. Я не ошибся, а высказался неверно. Я имел ввиду, что в конструкторах не работает механизм виртуальности.
Romeo, знаешь, у меня ощущение, что это не регламентировано стандартом. Т.е. это "компилятор-депенденд". Да, в Борланде и Микрософт, если мы сделает в конструкторе предка вызов виртуального метода, то при создании потомка, у которого этот виртуальны метод переопределен, будет вызван, тем-не менее виртуальный метод предка.
Но вот в TopSpeed C++ (с которым мне когда-то приходилось сталкиваться) из констуктора предка, таки, вызывался виртуальный метод потомка!
Аватара пользователя
Romeo
Сообщения: 3126
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

К сожалению, я ни разу не сталкивался с TopSpeed C++. По отзывам из интернета - это DOS компилер, который позиционировался, как самый быстрый и это его единственное отличие от того же, к примеру, BC 3.1. По крайней мере так вещает интернет.

Запрещает ли стандарт виртуализацию в конструкторах? Да, запрещает. И дело совсем не во вредности комитета по стандартизации: разрешение работы виртуальных вызовов создаёт неразрешимые проблемы на уровне архитектуры классов. Сейчас я приведу пример, когда программа упадёт, если в конструкторе будет разрешена виртуализация.

Код: Выделить всё

class A
{
public:
   A() : m_pIA(new int(0)) { Virt(5); }
   ~A() { delete m_pIA; }

   virtual void Virt(int i) { *m_pIA = i; }

private:
   int* m_pIA;
};

class B : public A
{
   B() : m_pIB(new int(0)) { /* do nothing */ }
   ~B() {delete m_bIB; }

   virtual void Virt(int i) { *m_pIB = i; }

private:
   int* m_pIB:
};
Рассмотрим какие вызовы происходят, а также выпишем значения полей. В конструкторе B вызовется сначала контруктор A, так что имеем следующее:

1. m_pIA(new int(0)) ----- m_pIA = pointer, *m_pIA = 0, m_pIB = uninitialized;
2. Virt(5) ---- срабатывает виртуализация и происходит вызов B::Virt(5);
3. *m_pIB = i ---- краш программы при попытке разыменования неинициализированного поинтера;
4. m_pIB(new int(0)) ---- m_pIB = pointer, *mp_IB = 0;

То есть виртуальный вызов вклинивается в последовательность вызов конструкторов и мы получаем краш, не дойдя до шага 4. Хотя если бы пункты 3 и 4 выполнились в обратном порядке (то есть 4, затем 3), то никаких бы проблем не возникло.

Это самый простой пример, его можно и усложнить. Но все подобные примеры будут объединены одной общей проблемой: через механизм виртуальности может быть вызван код, оперирующий с ещё неинициализированными объектами. Запрет же виртуализации в конструкторах прячет все методы, расположенные вниз по иерархии и разрешает вызывать в конструкторах только функции текущего класса или его предков, используя имя класса и оператор scope'а "::", что полностью разрешает упомянутую проблему.

Я не знаю как можно решить эту пробему в каком-то отдельно взятом компиляторе, например в том же TopSpeed С++, не попирая основ теории классов, а именно порядок вызова конструкторов. Потому делаю предположение, что ты что-то напутал и TopSpeed C++ работает точно также, как и другие компиляторы.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
Аватара пользователя
Romeo
Сообщения: 3126
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

From Rycharg:
--------------------------------------------------------------
в конструкторах нельзя вызывать виртуальные функции
Позвольте не согласится с Вами, Romeo.

Код: Выделить всё

class foo{
   public:
   virtual void blabla(){ cout << "bla"; }
   foo(){}
};

class foo2 : public foo{
   public:
   virtual void blabla(){ cout << "blablablabla"; }
   foo2() { this->foo::blabla(); }
};

From Romeo:
--------------------------------------------------------------

Код: Выделить всё

foo2() { this->foo::blabla(); }
Абсолютно согласен с претензиями. Я не ошибся, а высказался неверно. Я имел ввиду, что в конструкторах не работает механизм виртуальности. Это как раз то, что мешает нам написать такой код:

Код: Выделить всё

class A
{
public:
   A(int i) : m_i(0) { i = GetI(); }

   virtual int GetI() = 0;
protected:
   int m_i;

};

class B : public A
{
public:
   virtual int GetI() { return SpecificForBValue(); }
   ...
};
Из кода видно, что подобный виртуальный вызов (будь он возможен), разрешил бы наши проблемы с классами OneRoom и его наследниками.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
BBB
Сообщения: 1298
Зарегистрирован: 27 дек 2005, 13:37

Romeo писал(а):Я не знаю как можно решить эту пробему в каком-то отдельно взятом компиляторе, например в том же TopSpeed С++, не попирая основ теории классов, а именно порядок вызова конструкторов. Потому делаю предположение, что ты что-то напутал и TopSpeed C++ работает точно также, как и другие компиляторы.
Вряд ли напутал. Дело было давно, но я не "просто наткнулся", а специально ставил эксперимент по проверке последовательностей вызова. Тогда-то и отметил, что TopSpeed отличается от Borland (тогда еще C++ версия 3.1)
не попирая основ теории классов
Если насчет теории (т.е. абстаргируясь от языка программирования), так вот в Pascal-е (еще DOS-овском 5.5) вызов виртуального метода из конструктора вызывает метод потомка.

Ну и потом (возвращаясь именно к C++) это все не так сложно. Ведь вирт.метод вызывается по адресу, записнному в VMT. При создании C++-класса все равно ведь все начинается с вызова конструктора наследного класса (который, как я понимаю, сразу же передает управление на конструктор базового класса). Так что мешает именно в этот момент (до передачи управления в конструктор предка) заполнить VMT адресами вирт.методов класса-наследника? В Pascal-е именно так как-то и делается (я когда-то читал главу "Pascal inside" именно об этих объектных фишках, как это реализовано в Turbo Pascal; сейчас подробностей, понятное дело, уже не помню).
Тут правда конструктор предка (когда он будет вызван) должен понять, вызыван он из конструктора-потомка (и, стало быть, VMT уже заполнена) или же "напрямую" при создании экземпляра именно этого класса (не потомка). Но, думаю, это не сложно сделать. Например, посмотреть на таблицу VMT, если поля уже заполнены ненулевыми адресами, то перезаполнять их адресами своих методов уже не нужно (видимо, в Pascal-е как-то так и делается).
------------------
Далее. Хочешь, я приведу своего рода "контрпример", когда существующий в C++ порядок приводит к крашу программы? :) Код писать не буду, идея проста.
Объявляем pure-класс с виртуальным pure-методом, который вызывается из конструктора.
Объявляем класс-наследник, в которое этот виртуальный (pure в предке) метод переопределен и реализован.
В теле программы делаем вызов создания класса-наследника. Компилятор все это съест, т.к. класс-наследник уже не является pure, т.е. экземпляры этого класса создавать можно.
Но run-time в точке создания класса-наследника получим ошибку "Попытка вызова pure-метода". Финита ля комедия.
А если б виртуальность работала и в конструкторе, то такого падения не было бы :)

Вообще, чтобы не связываться со всем этим геморроем, есть простое решение. Конструктор делать пустышкой, а "инициализацию де-факто" (т.е. присвоение знаячений полям и какие-то необходимые алгоритмы) проводить в методе Init, который формально будет уже обычным методом (и поэтому в тем в т.ч. уже будут корректно отрабатывать виртуальные вызовы), но "де факто" (т.е. по смыслу, по выполняемой работе) будет как раз конструктором.
Аватара пользователя
Romeo
Сообщения: 3126
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

&quot писал(а):так вот в Pascal-е (еще DOS-овском 5.5)
Паскаль не стоит вообще трогать. В нём, если не ошибаюсь, вообще позволялось выбирать вызывать нам или не вызывать конструктор базового класса (точнее не конструктор, а резервированный метод Init). Паскаль вообще недоООП язык :)
&quot писал(а):Так что мешает именно в этот момент (до передачи управления в конструктор предка) заполнить VMT адресами вирт.методов класса-наследника?
Здесь ты напутал. Момент, когда таблица виртуальных методов заполнена или ещё не заполнена мы из кода своей программы отловить не можем никак по одной простой причине: все таблицы (по одной на каждый класс) заполнены ещё до входа в main, они статичные и не изменяются в ходе выполнения программы. Каждый объект содержит не таблицу, а поинтер на таблицу :)

Мне кажется, что ты даже не понял в чём заключается проблема, которую я описал. Проблема в том, что вызывается именно нужный виртуальный метод (метод наследника). Причём он вызывается из тела базового класса, а это значит, что до того, как было вызвано тело конструктора наследника, и отсюда в свою очередь следует, что если он будет обращаться к своим полям (он - это наследник), то получит массу неприятностей, так как они ещё не инициализированы. И исправить эту проблему не получится никак, так как если мы попытаемся вызвать другой метод, то нарушим идеологию виртуальности, так что остаётся только полностью запретить виртуальность в конструкторах.
&quot писал(а):Объявляем pure-класс с виртуальным pure-методом, который вызывается из конструктора.
Ты это пробовал скомпилировать? :) Я же говорю, что из конструктора по стандарту вызов методов через механизм виртуальности запрещён. То есть ты можешь вызвать функцию только не виртуально, указав имя класса и "::". По той причине, что предок ничего не знает о методах своего наследника, ты сможешь с таким способом вызвать только метод предка, либо метод предка-предка и так далее вверх по иерархии. Так что никакого сообщения "Попытка вызова pure-метода" не возникнет :)
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
BBB
Сообщения: 1298
Зарегистрирован: 27 дек 2005, 13:37

Romeo писал(а):Ты это пробовал скомпилировать? :) Я же говорю, что из конструктора по стандарту вызов методов через механизм виртуальности запрещён. То есть ты можешь вызвать функцию только не виртуально, указав имя класса и "::". По той причине, что предок ничего не знает о методах своего наследника, ты сможешь с таким способом вызвать только метод предка, либо метод предка-предка и так далее вверх по иерархии. Так что никакого сообщения "Попытка вызова pure-метода" не возникнет :)
Не только пробовал компилировать, но и компилировал. И не только компилировал, но и запускал (если не изменяет память, то в MS VC++ 6.0). И совершенно четко помню окно с сообщением о попытке вызове pure-метода (Это было уже довольно давно, но проверить еще раз проблемы нет, благо MS VC++ дома имеется).
И не вижу ничего (с точки зрения синтаксита С++) запретного в том, чтобы в теле конструктора написать вызов (вез всяких ::, а просто по одному имени) виртуального метода, объявленного pure в данном классе.
Аватара пользователя
Romeo
Сообщения: 3126
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

MS VC 6.0.

Код: Выделить всё

class A
{
public:
	A() { Virt(); }
	virtual void Virt() = 0;
};

class B : public A
{
public:
	B() { }
	virtual void Virt() {}
};

int main()
{
	B b;
	return 0;
}
Компилирутся, но не линкуется.

Код: Выделить всё

Test.obj : error LNK2001: unresolved external symbol "public: virtual void __thiscall A::Virt(void)" (?Virt@A@@UAEXXZ)
Debug/Test.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.
То есть он плюёт на виртуальность, чего и следовало ожидать, и пытается вызвать A::Virt. Но так, как виртуальность не работает, то нужно линковать имя метода с его телом.... а тела нет.

Если честно, я разочарован в MS VC 6.0. Студии, начиная с "семёрки", вообще отказываются это компилировать с надписью наподобие "Cannot call virtual function from constructor".
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
BBB
Сообщения: 1298
Зарегистрирован: 27 дек 2005, 13:37

Romeo,
Действительно, так, как ты написал - не линкуется.
Зато, если чуть хитрее, то получилось :)
Вот так (класс B и ф-я main - без изменений):

Код: Выделить всё

class A {
public:
    A () { AA (); };
    void AA () { Virt(); };
    virtual void Virt() = 0;
};
После запуска на stderr выводится:
runtime error R6025
- pure virtual function call
Аватара пользователя
Romeo
Сообщения: 3126
Зарегистрирован: 02 мар 2004, 17:25
Откуда: Крым, Севастополь
Контактная информация:

Мдя, это яркий пример того, как можно обмануть компилятор ради одной единственной идеи - завалить приложение :)

Нужно посмотреть ещё посмотреть этот код на более новых версиях компилятора. Вот только под рукой нет таковых.
Entites should not be multiplied beyond necessity @ William Occam
---
Для выделения С++ кода используйте конструкцию [ code=cpp ] Код [ /code ] (без пробелов)
---
Сообщение "Спасибо" малоинформативно. Благодарность правильнее высказать, воспользовавшись кнопкой "Reputation" в виде звёздочки, расположенной в левом нижнем углу рамки сообщения.
Аватара пользователя
Airhand
Сообщения: 239
Зарегистрирован: 06 окт 2005, 16:21
Откуда: Dnepropetrovsk

Всё очень просто: таблица виртуальных фунций создаётся после конструктора, поэтому вызов виртуальной функции в конструкторе не будет виртуальным, будет вызвана функция того класса, где производится вызов.
Оптимизация по скорости:
#define while if
Оптимизация по размеру:
#define struct union
Закрыто