Главная » Галлюциногенные » Виртуальный конструктор. Основные виды виртуальных конструкторов

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

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

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

#include
#include
#include
#include

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 << "~FireSkill()" << endl; }
<< "Katon!" << endl; }
<< "FireSkill::show()" << endl; }
<< "FireSkill:erase()" << endl; }
private :
friend class Skill;
FireSkill() { }
FireSkill(const FireSkill & ) ;
FireSkill & operator= (FireSkill & ) ;
} ;

class WoodSkill : public Skill
{
public :
~WoodSkill() { cout << "~WoodSkill()" << endl; }
virtual void cast() const { cout << "Mokuton!" << endl; }
virtual void show() const { cout << "WoodSkill::show()" << endl; }
virtual void erase() { cout << "WoodSkill::erase()" << endl; }
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 << le.what () << endl;
return EXIT_FAILURE ;
}

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

return EXIT_SUCCESS ;
}


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

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. Все объекты теперь создаются через один конструктор, и дальше мы будто бы работаем с объектом базового класса. Все виртуальные вызовы находятся внутри самого класса. Мы даже можем создать объект класса Skill в стеке - методы этого объекта все равно будут работать будто виртуальные.
  2. В конструкторе и деструкторе мы можем использовать виртуальный вызов методов.
  3. Базовый класс является, можно сказать, в каком-то роде абстрактным, потому что все его виртуальные методы должны быть переопределены в производных классах. Если этого не сделать, это приведет к тому, что, к примеру, mLetter->cast() будет ничем иным как попытка вызвать метод NULL-указателе.
  4. При вызове виртуального конструктора тип создаваемого объекта будет действительно определятся на этапе выполнения, а не на этапе компиляции. Однако такой вызов следует заключать в блок try-catch, иначе можно пропустить исключение.
  5. Если мы захотим добавить в базовый класс еще один виртуальный метод, придется переопределять его во всех производных.

Надеюсь, кому-нибудь пригодится;)

Теги:

  • cpp
  • виртуальный конструктор
Добавить метки

Узнав о виртуальных деструкторах, естественно спросить: "Могут ли конструкторы то же быть виртуальными?" Если ответить коротко - нет. Можно дать более длинный ответ: "Нет, но можно легко получить требуемый эффект".

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

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

Как правило "виртуальные конструкторы" являются стандартными конструкторами без параметров или конструкторами копирования, параметром которых служит тип результата:

expr(); // стандартный конструктор

virtual expr* new_expr() { return new expr(); }

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

class conditional: public expr {

conditional(); // стандартный конструктор

expr* new_expr() { return new conditional(); }

Это означает, что, имея объект класса expr, пользователь может создать объект в "точности такого же типа":

void user(expr* p1, expr* p2)

expr* p3 = p1->new_expr();

expr* p4 = p2->new_expr();

Переменным p3 и p4 присваиваются указатели неизвестного, но подходящего типа.

Тем же способом можно определить виртуальный конструктор копирования, называемый операцией размножения, но надо подойти более тщательно к специфике операции копирования:

// копировать `s" в `this"

inline void copy(expr* s);

// создать копию объекта, на который смотрит this

virtual expr* clone(int deep = 0);

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

Функцию clone() можно использовать, например, так:

void fct(expr* root)

expr* c1 = root->clone(1); // глубокое копирование

expr* c2 = root->clone(); // поверхностное копирование

Являясь виртуальной, функция clone() способна размножать объекты любого производного от expr класса. Настоящее копирование можно определить так:

void expr::copy(expression* s, int deep)

if (deep == 0) { // копируем только члены

else { // пройдемся по указателям:

left = s->clone(1);

right = s->clone(1);

Функция expr::clone() будет вызываться только для объектов типа expr (но не для производных от expr классов), поэтому можно просто разместить в ней и возвратить из нее объект типа expr, являющийся собственной копией:

expr* expr::clone(int deep)

expr* r = new expr(); // строим стандартное выражение

r->copy(this,deep); // копируем `*this" в `r"

Такую функцию clone() можно использовать для производных от expr классов, если в них не появляются члены-данные (а это как раз типичный случай):

class arithmetic: public expr {

// новых членов-данных нет =>

// можно использовать уже определенную функцию clone

С другой стороны, если добавлены члены-данные, то нужно определять собственную функцию clone():

class conditional: public expression {

inline void copy(cond* s, int deep = 0);

expr* clone(int deep = 0);

Функции copy() и clone() определяются подобно своим двойникам из expression:

expr* conditional::clone(int deep)

conditional* r = new conditional();

r->copy(this,deep);

void conditional::copy(expr* s, int deep)

if (deep == 0) {

expr::copy(s,1); // копируем часть expr

cond = s->cond->clone(1);

Определение последней функции показывает отличие настоящего копирования в expr::copy() от полного размножения в expr::clone() (т.е. создания нового объекта и копирования в него). Простое копирование оказывается полезным для определения более сложных операций копирования и размножения. Различие между copy() и clone() эквивалентно различию между операцией присваивания и конструктором копирования ($$1.4.2) и эквивалентно различию между функциями _draw() и draw() ($$6.5.3). Отметим, что функция copy() не является виртуальной. Ей и не надо быть таковой, поскольку виртуальна вызывающая ее функция clone(). Очевидно, что простые операции копирования можно также определять как функции-подстановки.

Страница 49 из 70

6.7.2 Указание размещения

По умолчанию операция new создает указанный ей объект в свободной памяти. Как быть, если надо разместить объект в определенном месте? Этого можно добиться переопределением операции размещения. Рассмотрим простой класс:
class X {
// ...
public:
X(int);
// ...
}; Объект можно разместить в любом месте, если ввести в функцию размещения дополнительные параметры:
// операция размещения в указанном месте:
void* operator new(size_t, void* p) { return p; } и задав эти параметры для операции new следующим образом:
char buffer;

Void f(int i)
{
X* p = new(buffer) X(i); // разместить X в buffer
// ...
} Функция operator new(), используемая операцией new, выбирается согласно правилам сопоставления параметров ($$R.13.2). Все функции operator new() должны иметь первым параметром size_t. Задаваемый этим параметром размер неявно передается операцией new.

Определенная нами функция operator new() с задаваемым размещением является самой простой из функций подобного рода. Можно привести другой пример функции размещения, выделяющей память из некоторой заданной области:

Class Arena {
// ...
virtual void* alloc(size_t) = 0;
virtual void free(void*) = 0;
};

Void operator new(size_t sz, Arena* a)
{
return a.alloc(sz);
} Теперь можно отводить память для объектов произвольных типов из различных областей (Arena):
extern Arena* Persistent; // постоянная память
extern Arena* Shared; // разделяемая память

Void g(int i)
{
X* p = new(Persistent) X(i); // X в постоянной памяти
X* q = new(Shared) X(i); // X в разделяемой памяти
// ...
} Если мы помещаем объект в область памяти, которая непосредственно не управляется стандартными функциями распределения свободной памяти, то надо позаботиться о правильном уничтожении объекта. Основным средством здесь является явный вызов деструктора:
void h(X* p)
{
p->~X(); // вызов деструктора
Persistent->free(p); // освобождение памяти
} Заметим, что явных вызовов деструкторов, как и глобальных функций размещения специального назначения, следует, по возможности, избегать. Бывают случаи, когда обойтись без них трудно, но новичок должен трижды подумать, прежде чем использовать явный вызов деструктора, и должен сначала посоветоваться с более опытным коллегой.

Страница 47 из 88

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

Узнав о виртуальных деструкторах, естественно спросить: "Могут ли
конструкторы то же быть виртуальными?" Если ответить коротко - нет.
Можно дать более длинный ответ: "Нет, но можно легко получить
требуемый эффект".
Конструктор не может быть виртуальным, поскольку для правильного
построения объекта он должен знать его истинный тип. Более того,
конструктор - не совсем обычная функция. Он может взаимодействовать
с функциями управления памятью, что невозможно для обычных
функций. От обычных функций-членов он отличается еще тем, что
не вызывается для существующих объектов. Следовательно нельзя получить
указатель на конструктор.
Но эти ограничения можно обойти, если определить функцию,
содержащую вызов конструктора и возвращающую построенный объект.
Это удачно, поскольку нередко бывает нужно создать новый объект,
не зная его истинного типа. Например, при трансляции иногда
возникает необходимость сделать копию дерева, представляющего
разбираемое выражение. В дереве могут быть узлы выражений разных
видов. Допустим, что узлы, которые содержат повторяющиеся в выражении
операции, нужно копировать только один раз. Тогда нам потребуется
виртуальная функция размножения для узла выражения.
Как правило "виртуальные конструкторы" являются стандартными
конструкторами без параметров или конструкторами копирования,
параметром которых служит тип результата:

Class expr {
// ...
public:
expr(); // стандартный конструктор
virtual expr* new_expr() { return new expr(); }
};

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

Class conditional: public expr {
// ...
public:
conditional(); // стандартный конструктор
expr* new_expr() { return new conditional(); }
};

Это означает, что, имея объект класса expr, пользователь может
создать объект в "точности такого же типа":

Void user(expr* p1, expr* p2)
{
expr* p3 = p1->new_expr();
expr* p4 = p2->new_expr();
// ...
}

Переменным p3 и p4 присваиваются указатели неизвестного, но подходящего
типа.
Тем же способом можно определить виртуальный конструктор
копирования, называемый операцией размножения, но надо подойти
более тщательно к специфике операции копирования:

Class expr {
// ...
expr* left;
expr* right;
public:
// ...
// копировать `s" в `this"
inline void copy(expr* s);
// создать копию объекта, на который смотрит this
virtual expr* clone(int deep = 0);
};

Параметр deep показывает различие между копированием собственно
объекта (поверхностное копирование) и копированием всего поддерева,
корнем которого служит объект (глубокое копирование). Стандартное
значение 0 означает поверхностное копирование.
Функцию clone() можно использовать, например, так:

Void fct(expr* root)
{
expr* c1 = root->clone(1); // глубокое копирование
expr* c2 = root->clone(); // поверхностное копирование
// ...
}

Являясь виртуальной, функция clone() способна размножать объекты
любого производного от expr класса.
Настоящее копирование можно определить так:

Void expr::copy(expression* s, int deep)
{
if (deep == 0) { // копируем только члены
*this = *s;
}
else { // пройдемся по указателям:
left = s->clone(1);
right = s->clone(1);
// ...
}
}

Функция expr::clone() будет вызываться только для объектов типа
expr (но не для производных от expr классов), поэтому можно просто
разместить в ней и возвратить из нее объект типа expr, являющийся
собственной копией:

Expr* expr::clone(int deep)
{
expr* r = new expr(); // строим стандартное выражение
r->copy(this,deep); // копируем `*this" в `r"
return r;
}

Такую функцию clone() можно использовать для производных от expr
классов, если в них не появляются члены-данные (а это как раз
типичный случай):

Class arithmetic: public expr {
// ...
// новых членов-данных нет =>
// можно использовать уже определенную функцию clone
};

С другой стороны, если добавлены члены-данные, то нужно определять
собственную функцию clone():

Class conditional: public expression {
expr* cond;
public:
inline void copy(cond* s, int deep = 0);
expr* clone(int deep = 0);
// ...
};

Функции copy() и clone() определяются подобно своим двойникам из
expression:

Expr* conditional::clone(int deep)
{
conditional* r = new conditional();
r->copy(this,deep);
return r;
}

Void conditional::copy(expr* s, int deep)
{
if (deep == 0) {
*this = *s;
}
else {
expr::copy(s,1); // копируем часть expr
cond = s->cond->clone(1);
}
}

Определение последней функции показывает отличие настоящего
копирования в expr::copy() от полного размножения в expr::clone()
(т.е. создания нового объекта и копирования в него). Простое
копирование оказывается полезным для определения более сложных
операций копирования и размножения. Различие между copy() и clone()
эквивалентно различию между операцией присваивания и конструктором
копирования ($$1.4.2) и эквивалентно различию между функциями
_draw() и draw() ($$6.5.3). Отметим, что функция copy() не является
виртуальной. Ей и не надо быть таковой, поскольку виртуальна
вызывающая ее функция clone(). Очевидно, что простые операции
копирования можно также определять как функции-подстановки.

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

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

Как известно, в языке программирования C++ нет прямой поддержки виртуального конструктора, однако, существует идиома, с помощью которой можно имитировать его работу. Прежде чем ее рассматривать, попробуем понять, каким поведением должен обладать виртуальный конструктор.

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

Однако, подобное поведение "ломается" непосредственно при создании объектов производных классов, а именно, при присваивании значению указателя на базовый класс адреса объекта производного класса обязательно нужно указывать этот производный класс. Наличие виртуального конструктора позволило бы клиентам с помощью конструктора базового класса создавать объекты производных классов, ничего не зная об их конкретных типах.

Фабричные методы, например, на базе обобщенного конструктора (паттерны Factory Method и Prototype), также предназначены для создания объектов без указания их конкретных типов, однако их поведение отлично от поведения виртуального конструктора.

Идиома, описанная Джеймсом Коплиеном, позволяет имитировать работу виртуального конструктора. Для этого она использует парадигму конверта, в котором лежит письмо. Класс конверта представляет собой базовый класс, содержащий указатель на объект того же базового типа. На практике этот указатель будет указывать на письмо, то есть объект некоторого производного класса. Когда клиент хочет создать объект некоторого типа, то он должен передать идентификатор этого типа в конструктор класса конверта. На основании этого идентификатора конструктор создает в куче объект соответствующего производного класса, адрес которого становится значением указателя на письмо. В дальнейшем все запросы от пользователя перенаправляются конвертом в письмо.

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

#include #include #include // Идентификаторы всех родов войск enum Warrior_ID { Infantryman_ID, Archer_ID, Horseman_ID }; class Warrior { public: Warrior(): p(0) { } Warrior(Warrior_ID id); virtual void info() { p->info(); } virtual ~Warrior() { delete p; p=0; } private: Warrior* p; }; class Infantryman: public Warrior { public: void info() { cout << "Infantryman" << endl; } private: Infantryman(): Warrior() {} Infantryman(Infantryman&); Infantryman operator=(Infantryman&); friend class Warrior; }; class Archer: public Warrior { public: void info() { cout << "Archer" << endl; } private: Archer(): Warrior() {} Archer(Archer&); Archer operator=(Archer&); friend class Warrior; }; class Horseman: public Warrior { public: void info() { cout << "Horseman" << endl; } private: Horseman(): Warrior() {} Horseman(Horseman&); Horseman operator=(Horseman&); friend class Warrior; }; Warrior::Warrior(Warrior_ID id) { if (id == Infantryman_ID) p = new Infantryman; else if (id == Archer_ID) p = new Archer; else if (id == Horseman_ID) p = new Horseman; else assert(false); } int main() { vector v; v.push_back(new Warrior(Infantryman_ID)); v.push_back(new Warrior(Archer_ID)); v.push_back(new Warrior(Horseman_ID)); for(int i=0; iinfo(); // ... }

Рассмотрим особенности реализации идиомы виртуального конструктора:

  1. Класс Warrior одновременно является основным классом для конверта и базовым классом для подклассов писем Infantryman, Archer, Horseman.
  2. Пользователи не могут непосредственно создавать воинов разных родов войск, так как конструкторы подклассов писем не являются общедоступными.
  3. Для создания воина некоторого типа используется конструктор конверта Warrior(Warrior_ID id). По полученному идентификатору типа в куче создается объект-воин, адрес которого и присваивается указателю на письмо. При этом в самом письме этот указатель не используется, поэтому при конструировании самого письма будет вызван конструктор по умолчанию Warrior(), который и инициализирует его нулем.
  4. Метод info() в базовом классе должен быть объявлен виртуальным для того, чтобы конверт мог автоматически перенаправить этот вызов в соответствующее письмо по указателю на объект базового класса.
  5. При разрушении конверта его деструктор освобождает память, занимаемую письмом. Это в свою очередь приводит к вызову деструктора базового класса Warrior для письма, где выполняется ничего не делающая команда delete 0.


Предыдущая статья: Следующая статья:

© 2015 .
О сайте | Контакты
| Карта сайта