Проблема с виртуальной функцией
От: Grenal  
Дата: 17.06.02 14:45
Оценка:
Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
Re: Проблема с виртуальной функцией
От: Sergey Россия  
Дата: 17.06.02 14:54
Оценка:
Здравствуйте Grenal, Вы писали:

G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


Нельзя. В конструкторе базового класса наследник еще не сконструирован.
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Re[2]: Проблема с виртуальной функцией
От: PaNov Россия  
Дата: 17.06.02 17:21
Оценка:
Здравствуйте Sergey, Вы писали:

S>Здравствуйте Grenal, Вы писали:


G>>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


S>Нельзя. В конструкторе базового класса наследник еще не сконструирован.



Кстати, недавно наступал на аналогичные грабли с точностью до наоборот...
Я в деструкторе базового класса вызывал виртуальную функцию, в надежде что она вызовет функцию, переопределенную в дочернем классе.
К тому времени отрабатывали деструкторы всех дочерних классов и по-всей видимости функции удалялись из таблицы виртуальных функций.
В результате — происходил вызов функции базового класса...
Re: Проблема с виртуальной функцией
От: OlegT  
Дата: 17.06.02 20:32
Оценка:
Здравствуйте Grenal, Вы писали:

G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


Конечно же, нет. Но я думаю, стоит объяснить "почему" (хотелось бы верить, что это прояснит многие похожие случаи). Как сказал, по-моему, Д'Аламбер "знание нескольких базовых фактов освобождает нас от запоминания множества следствий".
Поехали ...


class Base
{
 public:
   Base():mBase(1) { f(); };
   virtual void f() {};
   virtual void g() {};

   long mBase;
};

class Derived: public Base
{
 public:
   Derived():mDerived(2) { g(); };
   virtual void f() {};
   virtual void g() {};

   long mDerived
}

int main()
{
  ...

  Derived derived_Obj;

  ...
}


Что же происходит в строчке " Derived derived_Obj; "?
Прежде всего, примем к сведению, что к этому моменту уже созданы таблицы вирт. функций для классов Base и Derived. Обозначим указатели на них vtBase_ptr и vtDerived_ptr соответственно и допустим, что они находятся в начале каждого объекта ([объект в памяти] = [указ. на табл. вирт. ф-ций][данные] (если, конечно, у класса есть вирт. ф-ции)).
То есть, в нашем случае [derived_Obj] = [vtPtr][long][long].

Virtual Table класса Base:
vtBase_ptr[0] = указатель на функцию f класса Base (1)
vtBase_ptr[1] = указатель на функцию g класса Base (2)
Virtual Table класса Derived:
vtDerived_ptr[0] = указатель на функцию f класса Derived (3)
vtDerived_ptr[1] = указатель на функцию g класса Derived (4)

Итак ...
— Выделяется память(в стеке) размером [vtPtr]+[mBase]+[mDerived] = 12 байт и указатель на нее присваивается this'у
— *(this + 4 байта) = 1 { mBase = 1 }
— *(this) = vtBase_ptr { vtPtr = vtBase_ptr }
— вызываем конструктор класса Base
— вызываем функцию (*this)[0] { (vtBase_ptr[0])() , f() класса Base, см. (1) }
— выходим из конструктора класса Base
— *(this + 8 байт) = 2 { mDerived = 2 }
— *(this) = vtDerivedPtr { vtPtr = vtDerived_ptr }
— вызываем конструктор класса Derived
— вызываем функцию (*this)[1] { (vtDerived_ptr[1])() , g() класса Derived см. (4) }
— выходим из конструктора класса Derived

То есть, по-сути при конструировании объекта самого верхнего класса вызываются все конструкторы начиная с низа иерархии
и перед тем как войти в очередной конструктор класса Derived_i указателю на таблицу вирт. функций этого объекта присваивается указатель на таблицу вирт. ф-ций класса Derived_i. А внутри этого констр. происходит вызов, скажем, i-й виртуальной функции как (*(this)[i])( parameters ) .
В деструкторах же — все с точностью доо наоборот.

Н-да, получилось длинновато, но, надеюсь, понятно. (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).
Re[2]: Проблема с виртуальной функцией
От: Андрей Тарасевич Беларусь  
Дата: 17.06.02 20:59
Оценка: 21 (2)
Здравствуйте OlegT, Вы писали:

G>>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


OT>Конечно же, нет. Но я думаю, стоит объяснить "почему" (хотелось бы верить, что это прояснит многие похожие случаи). Как сказал, по-моему, Д'Аламбер "знание нескольких базовых фактов освобождает нас от запоминания множества следствий".

OT>Поехали ...

OT> ...


OT>То есть, по-сути при конструировании объекта самого верхнего класса вызываются все конструкторы начиная с низа иерархии

OT>и перед тем как войти в очередной конструктор класса Derived_i указателю на таблицу вирт. функций этого объекта присваивается указатель на таблицу вирт. ф-ций класса Derived_i. А внутри этого констр. происходит вызов, скажем, i-й виртуальной функции как (*(this)[i])( parameters ) .
OT>В деструкторах же — все с точностью доо наоборот.

OT>Н-да, получилось длинновато, но, надеюсь, понятно. :) (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).


Получилось действительно длинновато. А теперь загляни в код вызова виртуальной функции класса непосредственно из его конструктора или деструктора, сгенерированный, например, VC6, и ты обнаружишь, что все написанное тобой в этом случае не имеет никакого отношения к действительности.

Стандарт языка С++ требует, чтобы при вызове их конструктора/деструктора виртуальные функции вели себя так, как будто они являются невиртуальными. При вызове непосредственно из конструктра/деструкора именно так (т.е. невиртуально) и производит вызовы этих функций VC6 (да и другие компиляторы). Т.е. вызов статически направляется в точку входа функции и никакие таблицы виртуальных функций в процессе выполнения такого вызова никоим образом не участвуют.

При выполнении опосредованного вызова виртуальной функции из конструктора/деструктора (через посредство другой функции) все действительно происходит так, как ты описал. Конструкторы/деструкоры действительно подменяют указатель на VMT в экземпляре класса. Это делается для того, чтобы удовлетворить вышеупомянутому требованию стандарта С++ и при опосредованном вызове.

Твое объяснение является на самом деле ответом на вопрос "как?" ("Как компилятор ухитряется удовлетворить этому требованию стандарта языка С++ при опосредованном вызове?"), а не на вопрос "почему?". Причем твой ответ на вопрос "как?" является правильным только для ограниченного набора компиляторов. Какой-нибудь другой компилятор может реализовать эту функциональность по-другому.

А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++.
Best regards,
Андрей Тарасевич
Re[2]: Проблема с виртуальной функцией
От: George_Seryakov Россия  
Дата: 17.06.02 21:13
Оценка: 20 (3)
Здравствуйте OlegT, Вы писали:

OT>Конечно же, нет. Но я думаю, стоит объяснить "почему" (хотелось бы верить, что это прояснит многие похожие случаи). Как сказал, по-моему, Д'Аламбер "знание нескольких базовых фактов освобождает нас от запоминания множества следствий".


Это Гельвеций. Знание некоторых принципов легко возмещает незнание некоторых фактов.
GS
Re: Проблема с виртуальной функцией
От: potap  
Дата: 18.06.02 13:30
Оценка:
Здравствуйте Grenal, Вы писали:

G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


Вызови эту ф-цию в конструкторе производного класса и не пудри мозги
Re[3]: Проблема с виртуальной функцией
От: Аноним  
Дата: 18.06.02 20:10
Оценка:
Здравствуйте PaNov, Вы писали:

PN>Кстати, недавно наступал на аналогичные грабли с точностью до наоборот...

PN>Я в деструкторе базового класса вызывал виртуальную функцию, в надежде что она вызовет функцию, переопределенную в дочернем классе.
PN>К тому времени отрабатывали деструкторы всех дочерних классов и по-всей видимости функции удалялись из таблицы виртуальных функций.

Ошибка у вас в рассуждениях, однако.
'деструкторы' дочерних классов вызываются после отработки вашего кода. Поэтому причина не в этом, простейший пример поможет вам получить доказательства этого. К тому же как вы себе представляете 'удаление функций из таблицы виртуальных функций'(ТВФ) ? Как это сделать физически? :)

Пример:
class A
{
public:
~A() { TRACE("A::~A()\n"); }
};
class B : public A
{
public:
~B() { TRACE("B::~B()\n"); }
};

Что по-вашему, будет в Output окошке? Правилно, вот это:
B::~B()
A::~A()

Т.е. ваш собственный код (который вместо "TRACE("B::~B()\n");") вызывается до деструктора класса А. И ТВФ все еще валидна в этот момент, и правильная виртуальная функция будет вызвана.
Re[4]: Проблема с виртуальной функцией
От: Андрей Тарасевич Беларусь  
Дата: 18.06.02 20:22
Оценка:
Здравствуйте Аноним, Вы писали:

А>Здравствуйте PaNov, Вы писали:


PN>>Кстати, недавно наступал на аналогичные грабли с точностью до наоборот...

PN>>Я в деструкторе базового класса вызывал виртуальную функцию, в надежде что она вызовет функцию, переопределенную в дочернем классе.
PN>>К тому времени отрабатывали деструкторы всех дочерних классов и по-всей видимости функции удалялись из таблицы виртуальных функций.

А>Ошибка у вас в рассуждениях, однако.


А>Пример:

А>class A
А>{
А>public:
А> ~A() { TRACE("A::~A()\n"); }
А>};
А>class B : public A
А>{
А>public:
А> ~B() { TRACE("B::~B()\n"); }
А>};

А>Что по-вашему, будет в Output окошке? Правилно, вот это:

А>B::~B()
А>A::~A()

А>Т.е. ваш собственный код (который вместо "TRACE("B::~B()\n");") вызывается до деструктора класса А. И ТВФ все еще валидна в этот момент, и правильная виртуальная функция будет вызвана.


Читай внимательнее. "Дочерним классом" назывется класс-потомок, а не класс-предок. В данном случае дочерним классом является класс 'B'. Его деструктор вызывается первым. К моменту вызова деструкора класса 'A' деструкор дочернего класса 'B' уже закончил свое черное дело, как и сказал предыдущий оратор.

"Удаление функций из таблицы" — это, конечно, ерунда. Но порядок отработки деструкторов был назван совершенно правильно.
Best regards,
Андрей Тарасевич
Re: Проблема с виртуальной функцией
От: Hawk Россия  
Дата: 19.06.02 01:13
Оценка:
Здравствуйте Grenal, Вы писали:

G>Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию...


Конструктор базового класса предназначен _только_ для инициализации базового класса (то есть самого себя). У тебя же получается, что он потенциально может инициализировать и своих потомков (посредством виртуальной функции). Получается каша, в которой потом фиг разберешься, особенно если иерархия разрастется и этих загадочных связей накопится много. Такого быть, имхо, не должно и правильно компилятор делает, что бьет тебя по рукам.
Re[2]: Проблема с виртуальной функцией
От: Grenal  
Дата: 20.06.02 03:57
Оценка:
Здравствуйте OlegT, Вы писали:

OT>Здравствуйте Grenal, Вы писали:


OT>Н-да, получилось длинновато, но, надеюсь, понятно. :) (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).

Да вобщем я и сам себе это все представлял, только надеялся, что компилятор можно как-то обхитрить. Перенос вызова в дочерний класс не поможет. А задачу с помощью подобного вызова я пытаюсь решить следующую. У меня есть классы моей системы, все они наследуются от базового. И есть набор макросов типа RINTIME_CLASS и т.п. только проще.


#define DECLARE_COLLOBJECT_BASE(class_name, obj_type)public:  static CTVObject* PASCAL CreateObject()         {             CTVObject* pTVObject = new class_name();            if(pTVObject)            {                USES_CONVERSION;                WCHAR szTypeBis[256]={L""};                lstrcpynW(szTypeBis,pTVObject->GetType(),sizeof(szTypeBis)/sizeof(WCHAR));                pTVObject->SetSourceType( CharUpper(OLE2A(szTypeBis)) );            }            return  pTVObject;        }protected:    LPCWSTR GetType()    {        static LPCWSTR szType = OLESTR(#obj_type);         return szType;    }



class CTVObject: public CBase
{
public:
    CTVObject();
    virtual ~CTVObject();
    virtual LPCWSTR GetType() = 0;
}


GetType() — и есть тот виртуальный метод, который я вызываю в конструкторе, что бы заполнить одну из переменных CBase
В качестве решения проблемы смотрите реализацию CreateObject() — только не изящно это как-то.
Re[3]: Проблема с виртуальной функцией
От: OlegT  
Дата: 21.06.02 21:55
Оценка: 3 (1)
Здравствуйте Андрей Тарасевич, Вы писали:


АТ>... А теперь загляни в код вызова виртуальной функции класса непосредственно из его конструктора или деструктора, сгенерированный, например, VC6, и ты обнаружишь, что все написанное тобой в этом случае не имеет никакого отношения к действительности.

АТ>Стандарт языка С++ требует, чтобы при вызове их конструктора/деструктора виртуальные функции вели себя так, как будто они являются невиртуальными. При вызове непосредственно из конструктра/деструкора именно так (т.е. невиртуально) и производит вызовы этих функций VC6 (да и другие компиляторы). Т.е. вызов статически направляется в точку входа функции и никакие таблицы виртуальных функций в процессе выполнения такого вызова никоим образом не участвуют.

1) Естественно не имеет ... не надо принимать все так близко к сердцу :-). Это логика построения объекта, которую достаточно знать, чтобы понять практически все тонкие вопросы связанные с этим процессом.
2) Я бы еще поспорил как это происходило в самых первых версиях компиляторов С++(Скажем в том же CFront 1.0 — 2.0, первом официальном компиляторе с С++, который вообще генерировал С код (см. Страуструп "Дизайн и Эволюция С++", гл. 3.3.1 Генерирование С-кода, стр. 77)). И вообще, то что сейчас творят компиляторы, да еще с включенной оптимизацией, по моему неизвестно даже господу богу(попробуй предположить что сгенерит Intell C++ compiler version 5.1 в строчке z = x/y; и ты будешь довольно приятно удивлен.)
3) Да и не о коде, сгенерированном компилятором речь. Люди тут учатся, им совсем не интерестно, что думают об этом компиляторы, а твой ответ будет полезен им разве что при здаче какого-то экзамена по С++, преподу, фанату ассемблера, или же ... на Brainbench, все — и больше никому,


АТ>А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++.

И опять же .. неужели ты так наивно веришь, что яблоки падают на голову Ньютона,только потому, что он для них закон написал. Точно так же и тут, это правило в стандарте было высосано не из пальца умными дяденьками и тетеньками из комитетта по стандартизации, а имеет место только потому, что иначе строить иерархические древовидные конструкции не имеет никакого смысла.

Я, конечно, извиняюсь за определенную резкость суждений ... но "я категорически не согласен с твоими суждениями, хотя отдал бы жизнь за то, чтобы ты мог их высказывать"(по-моему Кант, но буду признателен, если кто-то уточнит :-) ).
Re[4]: Проблема с виртуальной функцией
От: Андрей Тарасевич Беларусь  
Дата: 22.06.02 05:02
Оценка: 10 (1)
Здравствуйте OlegT, Вы писали:

АТ>>А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++.

OT>И опять же .. неужели ты так наивно веришь, что яблоки падают на голову Ньютона,только потому, что он для них закон написал. Точно так же и тут, это правило в стандарте было высосано не из пальца умными дяденьками и тетеньками из комитетта по стандартизации, а имеет место только потому, что иначе строить иерархические древовидные конструкции не имеет никакого смысла.

Яблоки и язык С++ — две приниципиально разные вещи. Яблоки начали падать на головы значительно раньше, чем Ньютон придумал для них закон. Падение яблок объективно, а Ньютон лишь описал это явление являсь сторонним наблюдателем. Компиляторы же языка С++, смею заметить, не были обнаружены археологами в развалинах египетских пирамид и не прилетали с неизвестных планет в метеоритах. Поэтому нет никакой необходимости применять к этим компиляторам "исследовательский подход", тестируя их методом черного ящика и бессонными ночами ломая голову над интерпретацией результатов экспериментов. Компиляторы языка С++ реализуются дяденьками и тетеньками в штатах фирм разработчиков. Эти дяденьки и тетеньки прекрасно понимают, что компилятор должен быть реализован в соотвтетсвии с заветами и пожеланиями других дяденек и тетенек из комитета по стандартизации. Это лет десять назад можно было говорить, что стандарт языка С++ "растет" из его реализаций. С тех пор много воды утекло. В наше время реализация полностью определяется стандартом. Несоответствие реализации стандарту есть баг реализации, а не баг стандарта. Соотвествие реализауии стандарту — это не случайность, это результат сознательного дейсвия разработчиков компилятора. То, что виртуальные функции вызываются из конструктора объекта статически — это не результат оптимизаци, это наиболее простой и эффективный способ заставить программу вести себя так, как она и должна себя вести, согласно спецификации языка.

Насчет "иначе строить иерархические древовидные конструкции не имеет никакого смысла"- это совершенно неверно. (Конструкции эти, строго говоря, не обязательно древовидны, но дело не в этом.) Вопросы о возможности виртуального вызова виртуальной функции из конструктора и деструктора объекта возникают, разумеется, не на пустом месте. Несложно привести примеры ситуаций, в которых такая возможность была бы весьма полезна и, при соблюдении определенных "правил игры", вела бы себя совершенно корректно. Во многих случаях именно на эти ситуации натыкаются программисты, которые потом задают в конференциях пообные вопросы. Пример того же Borland Pascal 7 использует несколько иной протокол инициализации указателя на VMT в конструкоре класса и из его конструкоров виртуальные методы вызываются также как и из любых других, т.е. виртуально. Так что ни о каком "иначе ... не имеет никакого смысла" речи быть не может.
Best regards,
Андрей Тарасевич
Re: 2Hawk
От: Grenal  
Дата: 24.06.02 10:12
Оценка:
Здравствуйте Grenal, Вы писали:

G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?


После приведенных рассуждений и доказательств согласен с мнением Hawk. Свою сугубо специфичную проблему я решил с помочью дефайнов


#define DECLARE_COLLOBJECT_BASE(class_name, obj_type)public:  static CTVObject* PASCAL CreateObject()         {             CTVObject* pTVObject = new class_name();            if(pTVObject)                pTVObject->SetSourceType( #obj_type );            return  pTVObject;        }protected:    LPCWSTR GetType()    {        static LPCWSTR szType = OLESTR(#obj_type);         return szType;    }
#define DECLARE_COLLOBJECT(class_name, obj_type)    DECLARE_COLLOBJECT_BASE(class_name, obj_type)    DECLARE_RUNTIMECLASS(class_name, obj_type, CreateObject)

#define DECLARE_COLLOBJECTEX(class_name, obj_type, obj_typeex)    DECLARE_COLLOBJECT_BASE(class_name, obj_type)    DECLARE_RUNTIMECLASSEX(class_name, obj_type, CreateObject, obj_typeex)


Раньше
            pTVObject->SetSourceType( GetType() );

вызывался в конструкторе базового класса, а теперь сами видите виртуальная функция просто не используется (см. CreateObject())
Re[5]: Проблема с виртуальной функцией
От: Аноним  
Дата: 27.06.02 04:24
Оценка:
АТ>...Компиляторы же языка С++, смею заметить, не были обнаружены археологами в развалинах египетских пирамид и не прилетали с неизвестных планет в метеоритах.

:))) А где найдены развалины египетских пирамид?

И насчет прилета с планет в метеоритах тоже :)))
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.