Использование виртуальных функций в конструкторе.

Angry
Уже с Приветом
Posts: 1491
Joined: 02 Jul 2003 22:47

Использование виртуальных функций в конструкторе.

Post by Angry »

Как-то наткнулся на неприятную особенность с++ (воистину, неисчерпаемый язык! :) ) - невозможность использования виртуальных функций в конструкторе. Код, конечно откомпилируется, но в наследниках конструктор выполнятся будет используя функции суперукласса, а не самого наследника.
Интересно, как логически объяснить этот феномен. Я так понимаю, таблица виртуальных функций заполняется после отработки конструктора. Но зачем так сделано?
Hamster
Уже с Приветом
Posts: 11475
Joined: 20 Nov 2000 10:01
Location: Escondido, CA

Post by Hamster »

Можно посмотреть тут:

http://msdn.microsoft.com/library/defau ... uctors.asp

Если у вас есть

Code: Select all

class А
{
public:
A();
virtual void function();
};

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


К моменту отработки конструктора A объект B еще полностью не инициализирован, вызывать его функции опасно.
shadow7256
Уже с Приветом
Posts: 9392
Joined: 18 Mar 2004 15:11
Location: New York -> FL

Post by shadow7256 »

Нормальная практика. А как вызывать методы у еще не до конца достроенного объекта?

Мало ли что эти методы делают с членами класса (которых еще нет и в помине может быть) и от чего завивисит поведение внутри метода :umnik1:
Angry
Уже с Приветом
Posts: 1491
Joined: 02 Jul 2003 22:47

Post by Angry »

Я понял уже, что опасно. Вопрос, зачем так сделано проектировщиками языка?

Например, у меня есть базовый класс, который может инициализоватся совершенно по-разному. Я включаю в конструктор виртуальные функции, что бы в будущем через перегрузку этих функций решить эту проблему. Но таким способом эта проблема нерешаема. Как же поступить?
User avatar
Sabina
Уже с Приветом
Posts: 5669
Joined: 13 Oct 2000 09:01
Location: East Bay, CA

Re: Использование виртуальных функций в конструкторе.

Post by Sabina »

Angry wrote: Я так понимаю, таблица виртуальных функций заполняется после отработки конструктора. Но зачем так сделано?


Насколько я понимаю виртуальные функции, они и были созданы для того, чтобы при вызове этой функции для базового класса, программа динамически (at run time) определяла какую именно функцию derived class-а с таким же именем вызывать.

При этом в каждом derived class-е этот метод соответственно должен быть overrriden.

Сабина
Angry
Уже с Приветом
Posts: 1491
Joined: 02 Jul 2003 22:47

Post by Angry »

shadow7256 wrote:Нормальная практика. А как вызывать методы у еще не до конца достроенного объекта?

Мало ли что эти методы делают с членами класса (которых еще нет и в помине может быть) и от чего завивисит поведение внутри метода :umnik1:


Почему же вначале выделяется память под члены класс,отрабатывется код внутри конструктора, а потом заполняется VFT, хотя более логичным и удобным кажется выделение памяти, заполнение VFT, а потом уже отработка кода?
User avatar
Sabina
Уже с Приветом
Posts: 5669
Joined: 13 Oct 2000 09:01
Location: East Bay, CA

Post by Sabina »

Angry wrote:Я включаю в конструктор виртуальные функции, что бы в будущем через перегрузку этих функций решить эту проблему.


Там не overloading, а overriding.

Скажем у вас есть базовый класс Shape с функцией draw. Когда вы будете имплементировать Circle, Rectangular, Oval, YouNameIt эта функция будет переписана под каждый отдельный объект и вызываться будет именно она .

Сабина
Last edited by Sabina on 06 Jul 2004 23:04, edited 1 time in total.
ig
Уже с Приветом
Posts: 491
Joined: 09 Apr 2000 09:01
Location: Tigard, OR

Post by ig »

Потому что объект ещё не достроен.
Смотрите link
Last edited by ig on 06 Jul 2004 23:24, edited 1 time in total.
Angry
Уже с Приветом
Posts: 1491
Joined: 02 Jul 2003 22:47

Post by Angry »

ig wrote:Потому что класс ещё не достроен.
Смотрите link


Я же говорю, что это я понял.
Но кто-то может пояснить, почему так? Почему класс не достраивается до окончания работы конструктора? Чем это лучше, если бы все выделения памяти и заполнения VFT завершались до выполнения кода внутри конструктора?
Hamster
Уже с Приветом
Posts: 11475
Joined: 20 Nov 2000 10:01
Location: Escondido, CA

Post by Hamster »

Angry wrote: Я же говорю, что это я понял.
Но кто-то может пояснить, почему так? Почему класс не достраивается до окончания работы конструктора? Чем это лучше, если бы все выделения памяти и заполнения VFT завершались до выполнения кода внутри конструктора?


Порядок по-моему такой:

- Выделяется память
- VFT заполняется указателями на функции базового класса ( в моем примере, класс А )
- Вызывается конструктор A::A
- VFT перезаписывается указателями на функции субкласса ( B )
- Вызывается конструктор B::B

Вам придется вводить несколько конструкторов и вызывать в разных случаях разные конструкторы, или передавать какой-нибудь флаг, чтобы сказать конструктору, что ему делать.
shadow7256
Уже с Приветом
Posts: 9392
Joined: 18 Mar 2004 15:11
Location: New York -> FL

Post by shadow7256 »

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

Страуструп сделал правильную вещь, запретив использовать полиморфизм, когда дело касается конструкторов, иначе геморроя было бы гораздо больше.

Используйте паттерн "Фабричный метод". Он также известен под именем "Виртуальный констуктор". Он безопасен, элегантен и сделает то, что Вам надо.
User avatar
tengiz
Уже с Приветом
Posts: 4468
Joined: 21 Sep 2000 09:01
Location: Sammamish, WA

Post by tengiz »

Angry wrote:Но кто-то может пояснить, почему так? Почему класс не достраивается до окончания работы конструктора? Чем это лучше, если бы все выделения памяти и заполнения VFT завершались до выполнения кода внутри конструктора?

Потому, что выделить память и "заполнть VFT" (на самом деле заполняется указатель на VFT, который обчыно назвается vptr, - а VFT одна на класс, а не одна на экземпляр класса) - это ещё не значит построить объект. Построить объект - это выделить под него память, заполнить vptr и выполнить его конструктор. При создании объекта, полученного наследованием, память выделяется под весь объект один раз, но заполнение указателся на vptr и вызов констуктора подкласса делается для каждого класса в иерархии. Т.е. для упомянутого примера:

1. Выделили память под B (которая, разумеется, включает всё, что нужно для A).
2. Заполнили vptr указателем на A::VFT.
3. Вызвали A::A.
4. Заполнили vptr указателем на B::VFT.
5. Вызвали B::B.

Ваш вопрос, по сути, сводится к следующему - А чем это лучше вот такого порядка инициализации:

1. Выделили память под B.
2. Заполнили vptr указателем на B::VFT.
3. Вызвали A::A.
4. Вызвали B::B.

Ответ: ничем. Кроме того, что вариант, выбранный дизайнерами языка несколько более предсказуем и логичен в случае, если виртуальная функция f, переопределённая в B, использует какие-то члены родительского класса, справедливо рассчитывая, что A-то уже правильно проинициализирован.
Cheers
ig
Уже с Приветом
Posts: 491
Joined: 09 Apr 2000 09:01
Location: Tigard, OR

Post by ig »

Хочу привести пример. Если бы полиморфизм был бы доступен в конструкторе, то было бы невозможно гарантировать что произойдёт при вызове виртуальной функции. Кто-то наследует Ваш класс и overrides виртуальную функцию которую Ваш конструктор вызывает. Никаких гарантий, может быть всё что угодно.

Это как раз то, что Вы пытаетесь сделать с базовым классом. Как уже посоветовали, лучьше пользоваться factory.

Удачи
8K
Уже с Приветом
Posts: 5552
Joined: 20 Mar 2001 10:01
Location: SFBA

Re: Использование виртуальных функций в конструкторе.

Post by 8K »

Angry wrote:Как-то наткнулся на неприятную особенность с++ (воистину, неисчерпаемый язык! :) ) - невозможность использования виртуальных функций в конструкторе.

Про C# почитайте, как там конструкторы и инициализаторы работают.
Увидев друга, Портос вскрикнул от радости...
User avatar
Boriskin
Уже с Приветом
Posts: 18906
Joined: 30 Aug 2001 09:01
Location: 3rd planet

Re: Использование виртуальных функций в конструкторе.

Post by Boriskin »

Angry wrote: Интересно, как логически объяснить этот феномен.


Порядком создания объекта, а именно порядком вызовов конструкторов базовых классов и (что в данном конкретном случае не так важно) конствукторов переменных - членов класса.

ЗЫ Именно то, о чем Тенгиз написал.
Тупизна как Энтропия. Неумолимо растет.
chip700
Уже с Приветом
Posts: 672
Joined: 11 Apr 2001 09:01
Location: Russia, NN

Post by chip700 »

Еще могу добавить, что изначальная задача поставлена неверно.
Вы пытаетесь написать конструктор базового класса так, что бы он подошел на все случаи жизни - то есть его код без работал без изменений для потомков.
Но ведь когда мы создаем экземпляр класса потомка, мы вызываем именно его (потомка) конструктор. Так почему бы все что нужно изменить в поведении конструктора по отношению к родительскому не добавить в конструкторе потомка.

А если позволить в конструкторе родителя вызвать перегруженный потомком метод, то он получается будет вызван до вызова конструктора потомка. А если мы там инициализируем переменные, выделяем память для каких-либо внутренних структур, которые нужны для работы нашего перегруженного метода? Таким образом придется писать каждый перегруженный метод в стиле defensive programming, обрабатывая все возможные ошибки связанные с неинициализированными полями.
А как раз смысл конструктора в классе (по Страуструпу) - ГАРАНТИРОВАТЬ всем остальным методам класса, что на момент их вызова объект полностью проинициализирован.
Димма
Уже с Приветом
Posts: 660
Joined: 01 Aug 2002 08:09

Post by Димма »

Angry wrote:Но кто-то может пояснить, почему так? Почему класс не достраивается до окончания работы конструктора?


Дык по определению. Потому что конструктор достраивает класс.
Димма
Sergei VP
Новичок
Posts: 44
Joined: 21 Mar 2003 12:44

Post by Sergei VP »

Hamster wrote:
Порядок по-моему такой:

- Выделяется память
- VFT заполняется указателями на функции базового класса ( в моем примере, класс А )
- Вызывается конструктор A::A
- VFT перезаписывается указателями на функции субкласса ( B )
- Вызывается конструктор B::B

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


VFT вообще не используется в конструкторах, здесь виртуальные функции просто вызывается как обычные (невиртуальные). Поэтому и нет смысла строить VFT в run-time, что привело бы к потере эффективности, указатель на таблицу виртуальных функций определяется классом и он всегда известен на момент вызова конструктора.
Вообще-то использование виртуальных функций в конструкторах врядли можно назвать хорошей идеей, это может запутать и вас и ваших колег, так что лучше сделайте как-то по другому, воспользуйтесь паттерном фабрики класса если ваши объекты создаются только в куче (можно конечно создать через фабрику класса объект и в стеке но это немного сложнее).
User avatar
Nervous
Уже с Приветом
Posts: 7759
Joined: 18 Sep 2001 09:01
Location: RUS.76 -> KOR -> RUS.53 -> US.PA -> US.MD

Post by Nervous »

Возможно, вы будете еще раз неприятно удивлены, но и в деструкторах тоже не вызываются виртуальные методвы. По той же причине, только наоборот - объект уже частично разрушен (начиная с потомков к базам). Так шта....
N.E.R.V.O.U.S.: Networked Electronic Replicant Viable for Observation and Ultimate Sabotage.
User avatar
tengiz
Уже с Приветом
Posts: 4468
Joined: 21 Sep 2000 09:01
Location: Sammamish, WA

Post by tengiz »

Sergei VP wrote:VFT вообще не используется в конструкторах, здесь виртуальные функции просто вызывается как обычные (невиртуальные). Поэтому и нет смысла строить VFT в run-time, что привело бы к потере эффективности, указатель на таблицу виртуальных функций определяется классом и он всегда известен на момент вызова конструктора...

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

Return to “Вопросы и новости IT”