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

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

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

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

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

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

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

#include <iostream>

#include <vector>
#include <assert>

// Идентификаторы всех родов войск
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<Warrior*> 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; i<v.size(); i++)
        v[i]->info();
    // ...
}

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

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

results matching ""

    No results matching ""