Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
Здравствуйте Grenal, Вы писали:
G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
Нельзя. В конструкторе базового класса наследник еще не сконструирован.
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Здравствуйте Sergey, Вы писали:
S>Здравствуйте Grenal, Вы писали:
G>>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
S>Нельзя. В конструкторе базового класса наследник еще не сконструирован.
Кстати, недавно наступал на аналогичные грабли с точностью до наоборот...
Я в деструкторе базового класса вызывал виртуальную функцию, в надежде что она вызовет функцию, переопределенную в дочернем классе.
К тому времени отрабатывали деструкторы всех дочерних классов и по-всей видимости функции удалялись из таблицы виртуальных функций.
В результате — происходил вызов функции базового класса...
Здравствуйте 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 ) .
В деструкторах же — все с точностью доо наоборот.
Н-да, получилось длинновато, но, надеюсь, понятно. (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).
Здравствуйте OlegT, Вы писали:
G>>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
OT>Конечно же, нет. Но я думаю, стоит объяснить "почему" (хотелось бы верить, что это прояснит многие похожие случаи). Как сказал, по-моему, Д'Аламбер "знание нескольких базовых фактов освобождает нас от запоминания множества следствий". OT>Поехали ...
OT> ...
OT>То есть, по-сути при конструировании объекта самого верхнего класса вызываются все конструкторы начиная с низа иерархии OT>и перед тем как войти в очередной конструктор класса Derived_i указателю на таблицу вирт. функций этого объекта присваивается указатель на таблицу вирт. ф-ций класса Derived_i. А внутри этого констр. происходит вызов, скажем, i-й виртуальной функции как (*(this)[i])( parameters ) . OT>В деструкторах же — все с точностью доо наоборот.
OT>Н-да, получилось длинновато, но, надеюсь, понятно. :) (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).
Получилось действительно длинновато. А теперь загляни в код вызова виртуальной функции класса непосредственно из его конструктора или деструктора, сгенерированный, например, VC6, и ты обнаружишь, что все написанное тобой в этом случае не имеет никакого отношения к действительности.
Стандарт языка С++ требует, чтобы при вызове их конструктора/деструктора виртуальные функции вели себя так, как будто они являются невиртуальными. При вызове непосредственно из конструктра/деструкора именно так (т.е. невиртуально) и производит вызовы этих функций VC6 (да и другие компиляторы). Т.е. вызов статически направляется в точку входа функции и никакие таблицы виртуальных функций в процессе выполнения такого вызова никоим образом не участвуют.
При выполнении опосредованного вызова виртуальной функции из конструктора/деструктора (через посредство другой функции) все действительно происходит так, как ты описал. Конструкторы/деструкоры действительно подменяют указатель на VMT в экземпляре класса. Это делается для того, чтобы удовлетворить вышеупомянутому требованию стандарта С++ и при опосредованном вызове.
Твое объяснение является на самом деле ответом на вопрос "как?" ("Как компилятор ухитряется удовлетворить этому требованию стандарта языка С++ при опосредованном вызове?"), а не на вопрос "почему?". Причем твой ответ на вопрос "как?" является правильным только для ограниченного набора компиляторов. Какой-нибудь другой компилятор может реализовать эту функциональность по-другому.
А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++.
Здравствуйте OlegT, Вы писали:
OT>Конечно же, нет. Но я думаю, стоит объяснить "почему" (хотелось бы верить, что это прояснит многие похожие случаи). Как сказал, по-моему, Д'Аламбер "знание нескольких базовых фактов освобождает нас от запоминания множества следствий".
Это Гельвеций. Знание некоторых принципов легко возмещает незнание некоторых фактов.
Здравствуйте 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");") вызывается до деструктора класса А. И ТВФ все еще валидна в этот момент, и правильная виртуальная функция будет вызвана.
Здравствуйте Аноним, Вы писали:
А>Здравствуйте 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' уже закончил свое черное дело, как и сказал предыдущий оратор.
"Удаление функций из таблицы" — это, конечно, ерунда. Но порядок отработки деструкторов был назван совершенно правильно.
Здравствуйте Grenal, Вы писали:
G>Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию...
Конструктор базового класса предназначен _только_ для инициализации базового класса (то есть самого себя). У тебя же получается, что он потенциально может инициализировать и своих потомков (посредством виртуальной функции). Получается каша, в которой потом фиг разберешься, особенно если иерархия разрастется и этих загадочных связей накопится много. Такого быть, имхо, не должно и правильно компилятор делает, что бьет тебя по рукам.
Здравствуйте OlegT, Вы писали:
OT>Здравствуйте Grenal, Вы писали:
OT>Н-да, получилось длинновато, но, надеюсь, понятно. :) (И еще одно, тут приведена только общая схема, возможно с небольшими погрешностями ... но принцип работы она отражает).
Да вобщем я и сам себе это все представлял, только надеялся, что компилятор можно как-то обхитрить. Перенос вызова в дочерний класс не поможет. А задачу с помощью подобного вызова я пытаюсь решить следующую. У меня есть классы моей системы, все они наследуются от базового. И есть набор макросов типа RINTIME_CLASS и т.п. только проще.
class CTVObject: public CBase
{
public:
CTVObject();
virtual ~CTVObject();
virtual LPCWSTR GetType() = 0;
}
GetType() — и есть тот виртуальный метод, который я вызываю в конструкторе, что бы заполнить одну из переменных CBase
В качестве решения проблемы смотрите реализацию CreateObject() — только не изящно это как-то.
АТ>... А теперь загляни в код вызова виртуальной функции класса непосредственно из его конструктора или деструктора, сгенерированный, например, VC6, и ты обнаружишь, что все написанное тобой в этом случае не имеет никакого отношения к действительности. АТ>Стандарт языка С++ требует, чтобы при вызове их конструктора/деструктора виртуальные функции вели себя так, как будто они являются невиртуальными. При вызове непосредственно из конструктра/деструкора именно так (т.е. невиртуально) и производит вызовы этих функций VC6 (да и другие компиляторы). Т.е. вызов статически направляется в точку входа функции и никакие таблицы виртуальных функций в процессе выполнения такого вызова никоим образом не участвуют.
1) Естественно не имеет ... не надо принимать все так близко к сердцу :-). Это логика построения объекта, которую достаточно знать, чтобы понять практически все тонкие вопросы связанные с этим процессом.
2) Я бы еще поспорил как это происходило в самых первых версиях компиляторов С++(Скажем в том же CFront 1.0 — 2.0, первом официальном компиляторе с С++, который вообще генерировал С код (см. Страуструп "Дизайн и Эволюция С++", гл. 3.3.1 Генерирование С-кода, стр. 77)). И вообще, то что сейчас творят компиляторы, да еще с включенной оптимизацией, по моему неизвестно даже господу богу(попробуй предположить что сгенерит Intell C++ compiler version 5.1 в строчке z = x/y; и ты будешь довольно приятно удивлен.)
3) Да и не о коде, сгенерированном компилятором речь. Люди тут учатся, им совсем не интерестно, что думают об этом компиляторы, а твой ответ будет полезен им разве что при здаче какого-то экзамена по С++, преподу, фанату ассемблера, или же ... на Brainbench, все — и больше никому,
АТ>А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++.
И опять же .. неужели ты так наивно веришь, что яблоки падают на голову Ньютона,только потому, что он для них закон написал. Точно так же и тут, это правило в стандарте было высосано не из пальца умными дяденьками и тетеньками из комитетта по стандартизации, а имеет место только потому, что иначе строить иерархические древовидные конструкции не имеет никакого смысла.
Я, конечно, извиняюсь за определенную резкость суждений ... но "я категорически не согласен с твоими суждениями, хотя отдал бы жизнь за то, чтобы ты мог их высказывать"(по-моему Кант, но буду признателен, если кто-то уточнит :-) ).
Здравствуйте OlegT, Вы писали:
АТ>>А правильный ответ на вопрос "почему?" существенно проще и короче: потому что этого требует стандарт языка С++. OT>И опять же .. неужели ты так наивно веришь, что яблоки падают на голову Ньютона,только потому, что он для них закон написал. Точно так же и тут, это правило в стандарте было высосано не из пальца умными дяденьками и тетеньками из комитетта по стандартизации, а имеет место только потому, что иначе строить иерархические древовидные конструкции не имеет никакого смысла.
Яблоки и язык С++ — две приниципиально разные вещи. Яблоки начали падать на головы значительно раньше, чем Ньютон придумал для них закон. Падение яблок объективно, а Ньютон лишь описал это явление являсь сторонним наблюдателем. Компиляторы же языка С++, смею заметить, не были обнаружены археологами в развалинах египетских пирамид и не прилетали с неизвестных планет в метеоритах. Поэтому нет никакой необходимости применять к этим компиляторам "исследовательский подход", тестируя их методом черного ящика и бессонными ночами ломая голову над интерпретацией результатов экспериментов. Компиляторы языка С++ реализуются дяденьками и тетеньками в штатах фирм разработчиков. Эти дяденьки и тетеньки прекрасно понимают, что компилятор должен быть реализован в соотвтетсвии с заветами и пожеланиями других дяденек и тетенек из комитета по стандартизации. Это лет десять назад можно было говорить, что стандарт языка С++ "растет" из его реализаций. С тех пор много воды утекло. В наше время реализация полностью определяется стандартом. Несоответствие реализации стандарту есть баг реализации, а не баг стандарта. Соотвествие реализауии стандарту — это не случайность, это результат сознательного дейсвия разработчиков компилятора. То, что виртуальные функции вызываются из конструктора объекта статически — это не результат оптимизаци, это наиболее простой и эффективный способ заставить программу вести себя так, как она и должна себя вести, согласно спецификации языка.
Насчет "иначе строить иерархические древовидные конструкции не имеет никакого смысла"- это совершенно неверно. (Конструкции эти, строго говоря, не обязательно древовидны, но дело не в этом.) Вопросы о возможности виртуального вызова виртуальной функции из конструктора и деструктора объекта возникают, разумеется, не на пустом месте. Несложно привести примеры ситуаций, в которых такая возможность была бы весьма полезна и, при соблюдении определенных "правил игры", вела бы себя совершенно корректно. Во многих случаях именно на эти ситуации натыкаются программисты, которые потом задают в конференциях пообные вопросы. Пример того же Borland Pascal 7 использует несколько иной протокол инициализации указателя на VMT в конструкоре класса и из его конструкоров виртуальные методы вызываются также как и из любых других, т.е. виртуально. Так что ни о каком "иначе ... не имеет никакого смысла" речи быть не может.
Здравствуйте Grenal, Вы писали:
G>Есть два класса базовый и дочерний. В базовом задана виртуальная функция, в дочернем она переопределяется. Проблема в следующем в конструкторе базового класса я дергую за виртуальную функцию и вызывается функция базового класса, можно ли это как то вылечить, что бы конструкторе базового класса вызывалась реализация метода дочернего?
После приведенных рассуждений и доказательств согласен с мнением Hawk. Свою сугубо специфичную проблему я решил с помочью дефайнов
вызывался в конструкторе базового класса, а теперь сами видите виртуальная функция просто не используется (см. CreateObject())
Re[5]: Проблема с виртуальной функцией
От:
Аноним
Дата:
27.06.02 04:24
Оценка:
АТ>...Компиляторы же языка С++, смею заметить, не были обнаружены археологами в развалинах египетских пирамид и не прилетали с неизвестных планет в метеоритах.