Ооп может ли быть конструктор виртуальным

Обновлено: 01.05.2024

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

Зачем нам вообще нужен виртуальный деструктор, если хорошим тоном считают написание самостоятельного деструктора для всех классов(следовательно и для потомков, как я понимаю)?

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

Есть ли смысл вообще писать конструктор в абстрактном классе? ( по личному мнению - нет, но в примерах абстрактных классов конструкторы встречал ).

Какой смысл объявлять виртуальную функцию одного класса дружественной в другом?

3 ответа 3

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

В данном примере собственно основной "алгоритм" реализуется функцией execute() , которой нет никаких причин быть виртуальной. Все разнообразие поведений разных "программ" реализуется классами-наследниками через перекрытие функций init() , run() и done() , а сам "алгоритм" остается неперекрываемым.

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

Другими словами, в С++ конструктор - эта та самая функция, которая инициализирует механизм виртуальных вызовов. Поэтому сам конструктор вызвать виртуально невозможно - на этот момент функциональность виртуальных вызовов еще не проинициализирована.

Физически, в повсеместно используемой реализации полиморфизма, функциональность виртуальных функций обеспечивается так называемой таблицей виртуальных функций, указатель на которую хранится в каждом полиморфном объекте. Так вот конструктор, кроме прочего, как раз таки и занимается привязкой каждого полиморфного объекта к его правильной таблице виртуальных функций. До того, как такая привязка выполнена, виртуальность работать не может.

Хм. Совершенно верно у каждого класса - свой деструктор. Но виртуальность нужна именно для того, чтобы при полиморфном удалении объекта был правильно выбран именно правильный деструктор, в соответствии с динамическим типом удаляемого объекта. Т.е. если у вас есть указатель на базовый класс Base *p , который на самом деле указывает на объект наследного типа Derived

то при выполнении delete p (полиморфное удаление) вам нужно, чтобы был вызван именно деструктор класса Derived . Чтобы это произошло, деструктор должен быть виртуальным.

Если деструктор в такой ситуации невиртуален, то, как правильно заметил @VladD в комментариях, поведение программы не определено.

Создать самостоятельный экземпляр абстрактного класса нельзя потому, что спецификация языка это запрещает. Да и какой смысл в этом создании? Абстрактный класс - это класс с "несуществующими" (ненаписанными) виртуальными функциями. Зачем и кому могут понадобиться объекты такого неполноценного класса? Что вы предлагаете делать, если пользователь попробует вызвать такую несуществующую виртуальную функцию? Вызывать неопределенное поведение? Авторы языка решили, что разумнее просто запретить создание самостоятельных экземпляров абстрактных классов.

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

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

Вышеприведенный код приводит к неопределнному поведению (практически - к падению программы) именно из-за того, что во время работы конструктора класса S делается попытка вызова несуществующего метода S::foo() . Можно условно сказать, что в течение того "короткого мига", когда работает конструктор (или деструктор) абстрактного класса, соответствующий объект является самостоятельным абстрактным объектом со всеми вытекающими последствиями, как, например, падение программы при попытке вызова несуществующей виртуальной функции.

Не понимаю, почему возникает такой вопрос. Смысл писать конструктор в абстрактном классе, разумеется, есть, если для такого конструктора в этом классе есть работа. Конструктор должен что-то инициализировать. У вас есть что инициализировать в вашем абстрактном классе? Если есть - то пишите конструктор.

Другое дело, что обычно в абстрактных класса инициализировать нечего. Соответственно и конструктор чаще всего не нужен.

Не понимаю сути вопроса. Дружественность не имеет никакого отношения к виртуальности. Поэтому объявлять виртуальную функцию другом имеет ровно тот же смысл, что и объявлять любую другую (невиртуальную) функцию другом.

Тут стоит еще раз повторить, что дружественность ничего не знает о виртуальности (а виртуальность - о дружественности). Дружественность не распространяется по иерархии классов, т.е. объявленная вами дружественность никак не будет распространяться на "ту же самую" виртуальную функцию в классах-наследниках.

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

В данной статье я хочу затронуть вопрос виртуальности конструкторов, деструкторов, а также специфичные вопросы, так или иначе связанные с виртуальностью функций.

Статья расчитана на программистов средней и высокой квалификации. Приятного чтения.

Виртуальные конструкторы в C++

Итак, пожалуй начнем с конструкторов. Тут все очень просто — виртуальных конструкторов (а также похожих на них конструкторов) в C++ не существует. Просто потому что не бывает и всё тут (конкретно: это запрещено стандартом языка).

Вы, наверно, спросите: «а зачем такое вообще может понадобится?». На самом деле разговор про «виртуальный конструктор» немного некорректны. Конструктор не может быть виртуальным в смысле виртуальных функций, т.к. чтобы была виртуальность необходимо в конструкторе (а больше и негде особо) настроить указатель на ТВМ (таблицу виртуальных функций) для создаваемого объекта.

Замечание: обычно виртуальность реализуется через ТВМ и указатель на нее в объекте. Подробнее вы можете прочесть об этом тут

Так вот, иногда «виртуальным конструктором» называют механизм создания объект любого заранее неизвестного класса. Это может пригодится, например, при копировании массива объектов, унаследованных от общего предка (при этом нам бы очень хотелось чтобы вызывался конструктор копирования именно нужного нам класса, конечно же). В C++ для подобного, обычно, используют виртуальную функцию вроде virtual void assign (const object &o) , или подобную, однако, это не является повсеместным, и возможны другие реализации.

Виртуальный деструктор

А вот деструктор, напротив может быть виртуальным. И даже более того — это часто встречается.
Обычным является использование вирт деструктора в классах, имеющих вирт функции. Более того, gcc, например, выдаст вам предупреждение, если вы не сделаете виртуальным деструктор, объявив виртуальную функцию.

Часто можно встретить миф: «вирт деструктор нужен лишь в том случае, когда на деструктор классов-потомков возлагаются какие-то нестандартные функции, если деструктор потомка не отличается по функционалу от родителя, то делать его виртуальным нет особого смысла». Это может и будет работать «сейчас», но может сыграть злую шутку в будущем, да и в общем-то не очень верно. Если деструктор не виртуальный, то будет вызван деструктор того типа, какой заявлен в указателе. В тоже время будет правильнее что для объектов потомков должны вызываться свои деструкторы. Просто стоит принять это как правило, иначе в будущем могут быть очень большие проблемы с отладкой непонятно почему текучих в плане памяти программ.

Другой миф: чисто виртуальных деструкторов не бывает. Ещё как бывают.

* This source code was highlighted with Source Code Highlighter .


Существует миф, что данный класс является абстрактным. И это верно.
Также распространено заблуждение, что налсденики этого класса будут полиморфными. Это неверно — деструкторы не наследуются.
Существут миф, что наследкника этого класса создать нельзя. Можно, вот пример:

class Sample public :
virtual ~Sample()=0<> //обратите особое внимание сюда, так писать по стандарту нельзя, но MS VC проглотит
>;

class DSample: public Sample

* This source code was highlighted with Source Code Highlighter .

Для вменяемых компиляторов класс Sample нужно писать так:

Sample::~Sample() >

* This source code was highlighted with Source Code Highlighter .

Сразу же замечание про нормальность компилятора, и заодно миф: по стандарту определять чисто вирт. функцию внутри определения класса нельзя. Но определенные корпорации говорят «если мы видим возможность улучшить стандарт, то мы незадумываясь делаем это».

Вы ниразу не видели чисто виртуальных деструкторов с определенными «телами»? Так вот, миф что они несуществуют, также неверен. Также определять можно и другие чисто виртуальные функции.

Почему надо писать именно с определением деструктора? Ответ на самом деле прост: из налсденика DSample в его деструкторе ~DSample будет вызываться деструктор ~Sample, и поэтому его необходимо определить, иначе у вас это даже не будет компилироваться.

Для чего нужен подобный чисто виртуальный деструктор? Это используется для того, чтобы сделать класс абстрактным, не создавая чисто виртуальных функций. Другого применения я найти не смог.

Замечания об устройстве указаталей на функцию-член

Казалось бы данная часть не имеет отношения к виртуальности. Если вы так думаете, то сильно заблуждаетесь. Вообще говоря указатели на функцию член в C++ используются не так часто. Отчасти это связано с мутными (как мне кажется) главами в стандарте языка, отчасти, потому что их реализация выливается в полный кошмар для программистов компиляторов. Мне не известно ни одного компилятора, который мог бы работать с этими «штуками» полностью по стандарту.

Заключение

Вот, наверно, и все что я хотел бы рассказать вам в дополнение. Конечно, часть вопросов, связанных с виртуальностью все еще требуют открытий, оставляю это на вашей совести 8)

Удачи в программировании.

P.S. перенести бы в CPP блог надо бы… думаю там оно будет более востребовано.

Все мы знаем, что в C++ нет такого понятия как виртуальный конструктор, который бы собирал нужный нам объект в зависимости от каких-либо входных параметров на этапе выполнения. Обычно для этих целей используется параметризованный фабричный метод (Factory Method). Однако мы можем сделать «ход конем» и сымитировать поведение виртуального конструктора с помощью методики, называемой «конверт и письмо» («Letter/Envelope»).

Не помню, где я об этом узнал, но, если я не ошибаюсь, такую технику предложил Джим Коплиен (aka James O. Coplien) в книге «Advanced C++ Programming Styles and Idioms».

Идея состоит в том, чтобы внутри базового класса (Конверта) хранить указатель на объект этого же типа (Письма). При этом Конверт должен «перенаправлять» вызовы виртуальных методов на Письмо. С хорошими примерами у меня, как всегда, небольшие проблемки, поэтому «промоделируем» систему магических техник (или заклинаний) =) Предположим, что для каждой техники используется один из пяти основных элементов (а может и их комбинация), от которых зависит воздействие этой техники на окружающий мир и на предмет, к которому она применяется. В то же время мы хотим иметь возможность работать со всеми техниками независимо от их типа.

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

using std :: cout ;
using std :: endl ;

enum
<
FIRE = 0x01 ,
WIND = 0x02 ,
LIGHTNING = 0x04 ,
SOIL = 0x08 ,
WATER = 0x10
> ;

class Skill // aka Jutsu =)
<
public :
// virtual (envelope) constructor (see below)
Skill ( int _type ) throw ( std :: logic_error ) ;

// destructor
virtual ~Skill ( )
<
if ( mLetter )
<
// virtual call in destructor!
erase ( ) ;
>

delete mLetter ; // delete Letter for Envelope
// delete 0 for Letter
>

virtual void cast ( ) const < mLetter - >cast ( ) ; >
virtual void show ( ) const < mLetter - >show ( ) ; >
virtual void erase ( ) < mLetter - >erase ( ) ; >

protected :
// letter constructor
Skill ( ) : mLetter ( NULL )

private :
Skill ( const Skill & ) ;
Skill & operator = ( Skill & ) ;

Skill * mLetter ; // pointer to letter
> ;


class FireSkill : public Skill
<
public :
~FireSkill ( ) < cout
virtual void cast ( ) const < cout
virtual void show ( ) const < cout
virtual void erase ( ) < cout
private :
friend class Skill ;
FireSkill ( ) < >
FireSkill ( const FireSkill & ) ;
FireSkill & operator = ( FireSkill & ) ;
> ;


class WoodSkill : public Skill
<
public :
~WoodSkill ( ) < cout
virtual void cast ( ) const < cout
virtual void show ( ) const < cout
virtual void erase ( ) < cout
private :
friend class Skill ;
WoodSkill ( ) < >
WoodSkill ( const WoodSkill & ) ;
WoodSkill & operator = ( WoodSkill & ) ;
> ;


Skill :: Skill ( int _type ) throw ( std :: logic_error )
<
switch ( _type )
<
case FIRE :
mLetter = new FireSkill ;
break ;

case SOIL | WATER :
mLetter = new WoodSkill ;
break ;

default :
throw std :: logic_error ( "Incorrect type of element" ) ;
>

// virtual call in constructor!
cast ( ) ;
>


int main ( )
<
std :: vector < Skill * >skills ;

try
<
skills. push_back ( new Skill ( FIRE ) ) ;
skills. push_back ( new Skill ( SOIL | WATER ) ) ;
// skills.push_back(new Skill(LIGHTNING));
>
catch ( std :: logic_error le )
<
std :: cerr return EXIT_FAILURE ;
>

for ( size_t i = 0 ; i < skills. size ( ) ; i ++ )
<
skills [ i ] - > show ( ) ;
delete skills [ i ] ;
>

В принципе это не так интересно, но вывод будет следующим:

Katon!
Mokuton!
FireSkill::show()
FireSkill:erase()
~FireSkill()
WoodSkill::show()
WoodSkill::erase()
~WoodSkill()

Давайте лучше разберёмся, что же происходит.

Итак, у нас есть класс Skill (конверт), содержащий указатель на объект такого же типа (письмо). Конструктор копирования и оператор присваивания скроем в private от греха подальше. Основной интерес представляют два конструктора класса, один из которых открытый, а другой защищенный, а также деструктор.

Открытый конструктор, он же конструктор Конверта, он же в нашем случае «виртуальный конструктор» (его определение находится ниже), принимает один параметр — тип «элемента», на основе которого будет вычислен тип конструируемого объекта. В зависимости от входного параметра указатель на письмо инициализируется указателем на конкретный объект (FireSkill, WoodSkill и т.п., которые унаследованы от Skill). В случае, если во входном параметре неверное значение, выбрасывается исключение.

В производных классах техник FireSkill, WoodSkill и т.д. конструкторы по умолчанию закрыты, но базовый класс Skill объявлен как friend, что позволяет создавать объекты этих классов только внутри класса Skill. Конструктор копии и оператор присваивания в этих классах закрыты и не определены. Все виртуальные методы класса Skill переопределены в производных.

Виртуальный конструктор должен быть определен ниже всех производных классов, чтобы не пришлось париться с опережающими объявлениями (forward declarations), так как внутри него создаются объекты производных классов.

Когда в виртуальном конструкторе создаются объекты производных классов, при конструировании этих объектов сначала должен вызываться конструктор по умолчанию базового класса. Дефолтный конструктор базового класса ничего не делает, кроме инициализации нулем указателя на письмо. Фактически этот конструктор — конструктор письма, что мы и указываем, записывая в mLetter нуль.

Каким образом происходит вызов виртуальных методов? В базовом классе внутри виртуальных методов идет «перенаправление»: фактически Конверт играет роль оболочки, которая просто вызывает методы Письма. Так как методы Письма вызываются через указатель, то происходит позднее связывание, то есть вызов будет виртуальным. Более того! Мы можем виртуально вызывать методы в конструкторе и деструкторе: при создании объекта Skill (Конверта) происходит вызов параметризованного конструктора этого класса, который конструирует Письмо и инициализирует mLetter. После этого мы вызываем cast(), внутри которого стоит вызов mLetter->cast(). Так как mLetter на этот момент уже инициализирован, происходит виртуальный вызов.

То же самое в деструкторе ~Skill(). Сначала мы проверяем, проинициализирован ли mLetter. Если да, значит мы находимся в деструкторе Конверта, поэтому виртуально вызываем метод зачистки Конверта, а затем его удаляем. Если же нет, значит, мы в деструкторе Конверта, в котором выполняется delete 0 (а эта конструкция вполне безопасна).

Это вопрос из теста, а я не особо понимаю что значит виртуальный конструктор.

1 ответ 1

Если специалиста по C++ спросить "как сделать виртуальный конструктор в C++", то начинающий начнет что-то придумывать. Опытный, скорее всего, просто скажет: "это невозможно", но часто это скажет даже агрессивно (потому что его уже достали этим вопросом :) ). (дальше всё написано в контексте C++/Java).

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

Статический тип - это тип, который "виден" в объявлении переменной, а динамический - это фактический тип. Пример. Допустим, Foo - наследник Bar , и у нас есть такой код на Java:

Bar - статический тип, Foo - динамический.

Невиртуальные функции/методы вызываются согласно статическому типу, а вирутальные - динамическому. Но тут есть загвоздка - в Java все (или почти все?) методы виртуальные. А в C++ функции класса (да, так тут называются методы) по умолчанию не виртуальные.

А как же вызываются виртуальные функции/методы? Для этого почти везде используют так называемую "таблицу виртуальных методов", и каждый объект имеет указатель(ссылку) на экземпляр такой таблицы (нет смысла хранить в каждом объекте копию). И как только возникает необходимость сделать такой вызов - компилятор вставляет в код несколько инструкций, которые ищут подходящую функцию/метод в этой таблице. Это называется поздним связыванием, и обычно оно медленнее, чем раннее связывание (а оно происходит, когда компилятор знает или догадался, какая функция/метод будет вызвана в определенной точке кода, даже если это виртуальная функция/метод).

Это всё была теория, из которой нужно вынести две главные вещи: виртуальную функцию/метод можно вызвать только тогда, когда у нас есть указатель на реальный (созданный) объект и в нем есть таблица с подходящей функцией/методом. Теперь ответ на вопрос становится прост - виртуальный конструктор невозможен, так как на момент его вызова объекта ещё просто не существует - ещё даже не существует указателя.

Но откуда пошло всё это? А пришло это с Delphi. Там есть такая штука как метакласс. И с помощью метакласса можно передать ссылку на класс (да, ссылка на класс, не на объект) в функцию или метод, где код может создать класс, не зная о том, какой реальный класс. А потом появился C++ Builder, куда притащили VCL, и пришлось немного "расширить C++". А дальше понеслось. Поэтому опытные C++ программисты так взбудораживаются, когда слышат "виртуальный конструктор" - это всё Delphi.

Но если бы теоретически "виртуальный конструктор" существовал, то зачем он мог бы быть нужен? Самая простая причина - десериализация. Представим себе функцию/метод, которая на вход получает XML/JSON и каким-то магическим образом догадывается, какой объект был там сериализован, и возвращает объект нужного типа. Более того, в Java это используется. Просто называется по-другому. А называется это Фабричный метод.

Почему в Java нет виртуальных конструкторов, да и вообще - виртуальных статических методов? Идеологические или технические ограничения? В Delphi например есть - весьма удобная вещь.

Вопрос не совсем по адресу. это вам наверно к разработчикам Явы, которых здесь вы наверно не найдете) Вопрос вам: А зачем? Я как-то без таких вещей обхожусь. Даже по секрету скажу - я не знаю что это такое и не хочу знать, если я без этого проживу)

Сразу заметно, что человек перешел с С и еще не освоился. У меня есть знакомый, он сишник. Такой ярый приверженец:) Ни раз с ним холивары разводили. Ему подсунули проект на java + c++, он поплевался, поругался, понервничал. Потом через год говорит, - "на жава проще" :) Жду очередного "разочарования" :0 p.s. По теме: я тоже хз что за виртуальный конструктор и у меня есть подозрение: раз он вам надо, значит вы что-то пытаетесь сделать не так :)

2 ответа 2

В Java для реализации полиморфизма используется позднее связывание. Поэтому все методы по умолчанию виртуальные, кроме static, private, final.

А вообще, что вы подразумеваете под "виртуальным статическим методом"?

Виртуальный статический метод - значит static и его можно overload в наследнике. Конструктор является в некотором роде тоже статическим методом, но его перегрузка запрещена в Java и C++. Реализация по идее не должна быть сложной - просто добавить VMT не только к классу, но и к метаклассу, и при вызове статических методов передавать не this (указатель на объект класса), а указатель на объект метакласса. Как это и сделано в Delphi.

с какого это перепуга перегрузка конструктора в java и с++ запрещена. и что есть overload? может быть override?

Как я понимаю, если бы были виртуальные конструкторы (и виртуальные статические методы), мы могли бы писать нечто в этом духе:

Таким образом, имея ссылку типа класс А на класс B, мы можем полиморфно сздавать объекты класса B. В Java это достигается абстрактными фабриками. Почему не сделали, как в Delphi (помимо того, конечно, что Delphi это не эталон :))? Возможно, потому что так не сделали в С++ - и такое добавление, на мой взгляд заставило бы существенно пересмотреть объектную модель, сделав ее значительно отличающейся от C++.

Да, одно из удобных применений виртуальных конструкторов - именно такое. Delphi конечно не эталон, но несмотря на то что на него все фыркают - не холивара ради - всё-таки весьма удобная вещь, в которой реализованы вещи, отсутсвтующие в майстримных C++ и Java. Навскидку - property, события, finally (нет в C++), виртуальные статические методы, даже лямбды/замыкания есть (в Java пока ещё разрабатываются). В Delphi Prysm есть куча расширений языка (это вообще даже другой язык), например - констрактное программирование (пред- и пост- условия на методы).

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

Имел возможность работать на разных языках, поэтому не привязан ни к какому "лагерю", поэтому адекватно оцениваю достоинства и недостатки различных языков. property - конечно на вкус и цвет, но на мой - очень удобная вещь для компонентного программирования, в частности - для визуального. И в EJB вроде даже есть неуклюжая имитация свойств, основанная на naming-convenstions. И в других языкях на JVM виртуальные статические методы не могут быть, т.к. их нет в самой JVM. А по языкам я вообще присматриваюсь к D - идеальный язык для натива, включает всё лучшее что существует на сегодняшний день.

Читайте также: