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

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



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

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

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

    #include <cstdlib>
    #include <iostream>
    #include <stdexcept>
    #include <vector>

    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; }
        virtual void cast() const { cout << "Katon!" << endl; }
        virtual void show() const { cout << "FireSkill::show()" << endl; }
        virtual void erase() { cout << "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. Если мы захотим добавить в базовый класс еще один виртуальный метод, придется переопределять его во всех производных.


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

    FIN.

    Progg it
    Поделиться публикацией

    Комментарии 48

      –2
      Забавно было бы посмотреть, а кому реально это пригодится и в каком случае.

      Обычно, как вы правильно заметили, процесс создания множества разных объектов одним методом делается либо с помощью фабричного метода (BaseClass* obj = BaseClass::Create(...)), либо с помощью абстрактной фабрики (BaseClass* obj = Fabric::Get(...)->Create(...)). А такое чудо-юдо я даже не знаю, где и применить.

      Кстати, техника «конверта» и «письма» — это практически в чистом виде классический плюсовый smart pointer.
        +2
        Кстати, техника «конверта» и «письма» — это практически в чистом виде классический плюсовый smart pointer.
        Не совсем. Здесь конверт и письмо — это один и тот же класс. А умный указатель — это отдельный класс, который вообще ничего не знает о типе объекта, который в нём хранится.
          0
          Моё дело познакомить Вас с такой возможностью, а использовать её или нет — решать Вам.

          Кстати, «письмо и конверт» — это, можно сказать, особый вид паттерна State.
            0
            Или даже Proxy?
            • НЛО прилетело и опубликовало эту надпись здесь
                +2
                Взаимно.
                • НЛО прилетело и опубликовало эту надпись здесь
                    –1
                    «…чтобы обеспечить заявленную логику, базовый класс ДОЛЖЕН знать о всех потомках, которые будут от него унаследованы когда-нибудь».

                    Это плата за то чтобы клиент базового класса о потомках даже не подозревал.
                      –1
                      Это плата за то что все конструкторы наследников — приватные. А зачем?
                        0
                        Поясню свою мысль:

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

                        Зачем такое ограничение? В данном примере объекты-то получатся взаимозаменяемыми, вне зависимости от того, созданы ли они через Skill(int) или непосредственно.

                        Если это ограничение убрать, и конструкторы сделать открытыми, friend не нужен.
                          0
                          Если открыть конструкторы производных классов, то можно будет создавать объекты классов не через виртуальный конструктор. Если Вас такое поведение устраивает, то Вам вообще нет смысла использовать такую технику.
              0
              Фабрика в данном случае не причем. Вирутальный конструктор реализуется посредством виртуальной ф-ии clone(), которая возвращает указатель на реально содержащийся в классе тип, а никак не посредством фабрики.
                +3
                Фабрика вообще-то самый популярный способ реализации «виртуального конструктора». А метод clone (оно же паттерн прототип) это по сути некий аналог аналог «виртуального» конструктора копии, более узкий случай.
                  0
                  Не хочу с Вами спорить, но фабрика и виртуальный консруктор немного разные вещи, хотя грань и не велика и многие, по ошибке называют Виртуальный конструктор Фабрикой.
                  Три ссылки, которые рассматривают виртуальный конструктор: раз, два, три
                    0
                    Спорить смысла нету — это вопрос опредения понятия «виртуальный конструктор», которое отсутствует в С++. Фабрики ближе к понятию «метакласс», но по большому счету оба паттерна по-своему подходят под это название.
              –3
              Вообщето весь прогркссивный мир пдля такиз цеоей пользуется фабриками. То, что вы тут накуралесили — ЕРЕСЬ.
                –1
                Во-первых, это не накуралесил, а Coplien.

                А во-вторых, я не заставляю Вас пользоваться описанной методикой. Если она Вам не подходит, то это еще не означает, что она бесполезна. У всех паттернов есть свои достоинства и недостатки.
                +1
                Спасибо за то что поделились необычной техникой.

                Я ещё не придумал как это всё можно использовать, но зато в голову пришла безумная мысль. Скажем, в конверте может храниться не один указатель, а несколько. Например, в случае водно-земельного заклинания конверт мог бы хранить указатели на водное и земляное заклинания. В идеале, вызов skills[i]->Attack(); должен вызывать виртуальные функции-члены для всех внутренних указателей. К сожалению, так красиво сделать не удастся, зато вот такой вызов реализовать вполне можно: skills[i]->Make(&Skill::Attack);

                Конечно, приведённый пример скорее из серии эквилибристики, чем реально полезных вещей. Хотя кто знает… )
                  0
                  Немного нужно порефакторить, но в целом вот: dumpz.org/10589/
                    0
                    Да, примерно об этом я и говорил. Вот только описывать базовый вызов для каждой функции очень муторно и потенциальный источник ошибки. Есть вариант сделать это банально через пропроцессор, но я бы предпочёл чуть более сложный в вызове универсальный вариант:
                    void Skill::Make (void (Skill::*foo)())
                    {
                        for (size_t i = 0; i < mLetters.size(); ++i)
                            (mLetters[i]->*foo)();
                    }
                    
                    ...skills[i]->Make(&Skill::Foo);

                      0
                      Да, примерно это я и подразумевал под «порефакторить» ;)
                        0
                        Классно! Теперь бы придумать где такое можно применить :)
                          0
                          Можно, к примеру, сделать лексический анализатор, который будет использовать виртуальный конструктор для создания определенного вида лексем на основе какой-то входной инфы…
                  +1
                  а в чем плюсы то такого полета фантазии?
                  по сравнению с фабриками мы лишаемся наглядности.
                  параметр, который на самом деле представляет тип — это ахтунг и фундамент для долгих дебагов, ибо лишает нас помощи компилятора и переносит контроль типов на наши плечи. одно изменение — и компилятор не сообщит обо всех ошибках. тока Control+F. А если кто дефайнул констант по-своему…
                  Базовый класс, который знает наследников — это пять… Игра явно не стоит свеч.
                    +6
                    я бы отнёс подобный код к запретным техникам (kinjutsu, 禁術)… не изящный он какой-то
                      0
                      О киндзюцу тоже надо иметь представление ;)
                      +3
                      спасибо, способ оригинальный и статья полезная, лучше кучи других в которых описываются элементарные вещи, которые легко находятся в любом учебнике.
                        0
                        Достаточно странный метод, если честно.
                        Вся история с виртуальными функциями придумана чтобы избежать switch...case по типам. А тут он в явном виде присутствует :(
                          +1
                          В книге GoF при реализации параметрического Фабричного Метода тоже используется набор if-ов…
                            0
                            Пардон, параметризованного
                          0
                          Вообще само по себе определение «виртуальный конструктор» не совсем корректно — виртуальный метод выбирается зависимо от реальной интстанции объекта стоящего за указателем на базовый класс. К конструктору то применить вообще-то нельзя :)

                          С другой стороны, если пойти в торону метаклассов, то конструктор есть методом метакласса, и тогда надо делать объект Type с виртуальным методом CreateInstance. Получим абстрактную фабрику.

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

                          Впрочем это все философия на тему что такое «виртуальный конструктор» вообще. Термин не определен языком С++, так что спорсить особо некуда :)
                            0
                            В этом случае я бы предпочел другую формулировку виртуального метода: виртуальный метод — это метод, нужная версия реализации которого выбирается на этапе выполнения, а не на этапе компиляции.
                            0
                            А кто-нибудь встречал Коплиена в электронном варианте? Я не нашёл.
                              0
                              Jim Coplien значится в рецензентах и «благодарностях» к многим книгам других известных авторов, даже у Страуструпа по-моему… Так что, возможно, Вы его уже читали :)
                                0
                                Нет я уверен, что читал Страуструпа, Мейерса, Александреску, Эккеля, Гамму, Хелма, Влиссидеса и других, но Коплиена среди них не было ;). Хотелось бы найти всё-таки эти книги — Advanced C++ и Multiparadigm Design for C++. Гугль настойчиво предлагает купить :(
                              0
                              Я не буду кричать, что это ересь или бред. Просто потому, что это ни разу не так.
                              Необычно — да. Я о такой возможности не задумывался. Может быть, где-то пригодится.
                              В любом случает, спасибо автору статьи за труд :)
                                0
                                не совсем понял вот это:
                                >Виртуальный конструктор должен быть определен ниже всех производных классов,
                                >чтобы не пришлось париться с опережающими объявлениями (forward declarations), так
                                >как внутри него создаются объекты производных классов.

                                это вы вообще, или про конкретную вашу реализацию?
                                  0
                                  Это я конкретно про текущую реализацию.
                                  0
                                  А если мне нужно будет через пол года добавить параметризованный конструктор в наследника?
                                    0
                                    Если есть шанс того, что Вы будете вносить подобные изменения, их нужно предусмотреть на этапе проектирования, а значит — подобный «мегахак» не для Вас.
                                      0
                                      Всего не предусмотришь. Я не сторонник мегапроектирования всей системы сразу и навеки. Лучше заранее избегать таких вот «мегахаков» и использовать гибкие решения, чтобы потом не пришлось переписывать всю систему.
                                        0
                                        Я не говорил о мегапроектировании, но лучше все-таки хотя бы локализовать область возможных изменений.

                                        Я приверженец идеи о том, что на любую подобную технику найдется своё применение.
                                    –1
                                    У этого паттерна гораздо больше недостатков, чем у аналогичных. В нем есть практики того, как делать не надо.
                                    И все эти люди, которые пишут о том что он плохой, делают это не просто так.

                                    Поэтому чтобы не совращать неокрепшие умы, стоит добавлять к этому паттерну приставку «анти».
                                      0
                                      Я не против, ибо в принципе я и не приписывал такой подход к паттернам.
                                      +2
                                      Все думал, думал чтоже мне напоминает данный паттерн. И вот вспомнил Curiously recurring template pattern (http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). И как я удвился когда увидел, что автор тоже Джим Коплиен. Хочу сказать, что CRTP действительно интересный паттерн и когда-то сам его использовал. Слышал что в Adobe его пользовали и даже вроде в QT. А в статье видимо его динамический аналог.

                                        0
                                        И всё-же это ересь и бред. Да, так написать можно, но за такой код надо убивать, а потом увольнять. Он противоречит основным парадигмам ООП: получается, что базовый класс обязан знать всё о своих наследниках — сколько их, как они называются, сколько в них функций, с какими параметрами и т.д. И каждый раз при добавлении нового класса\функции нужно будет переписывать вообще всё, включая базовый класс и его конструктор.

                                        Этот пример красиво и смачно плюёт на всё ООП в целом (и особо ярко на наследование с полиморфизмом в частности), забивает микроскопом гвозди и единственная от него польза — объяснить почему нужно пользоваться паттерном «фабрик» и не изобретать таких вот велосипедов-монстров.
                                          0
                                          Ересь и бред — это сначала убивать, а потом увольнять ;)

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

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое