Старая новая техника: CRTP

В последнее время в сети всё больше встречается вопрос: а что такое CRTP? А зачем оно нужно? А есть ли смысл его использовать? С другой стороны, растёт и количество статей пытающихся ответить на вопросы страждущих. Только ленивый ещё не написал свою версию статьи по CRTP., поэтому,  чтобы не прослыть ленивым, я тоже решил внести свою лепту. Интересным в этой ситуации является то, что идее, которая была сформулирована под аббревиатурой CRTP, уже без малого 19 лет. И она довольно широко использовалась на протяжении всех этих(да и предшествующих статье) лет. Так что непонятно чем вызван такой неподдельный интерес со стороны сообщества. Может быть, действительно, с выходом нового стандарта, интерес к C++ значительно вырос? В настоящей статье я попробую раскрыть смысл идеи CRTP, а так же ответить на самый главный вопрос: стоит ли его использовать? Начнём же мы, пожалуй, с экскурса в историю.

Что за зверь?

Название CRTP(curiously recurring template pattern) появилось благодаря одноименной статье написанной в далёком 1995 году. Интересно, что аббревиатура CRTP для многих стала названием шаблона проектирования(паттерна). Но правда заключается в том, что изначальная статья как раз и описывала некий ШП с применением шаблонов C++(template pattern), который удивительно(curiously) часто встречался(recurring) автору оригинального документа. А значит автор не был ни “изобретателем” этой модели, ни её апологетом: он просто свёл несколько примеров воедино и представил результат сообществу. Таким образом, на мой взгляд, CRTP является не лучшим названием самого ШП. Более того, я считаю, что к проектированию данная техника отношения вообще не имеет, поэтому далее, о CRTP, я буду говорить как о технике, а не ШП.

Что же из себя представляет CRTP? CRTP можно описать буквально в одном предложении: CRTP моделирует ситуацию при которой класс-наследник наследует класс-родитель параметризированный классом-наследником, или кодом:

template<typename T>
class Base
{
    ...
};

class Derived: public Base<Derived>
{
    ...
};

Как вы можете видеть реализация довольно проста, но, в то же время выглядит немного сбивающей с толку. И это неудивительно, что многие не понимают зачем такое может быть использовано. Особенно часто такие вопросы возникают у новичков в программировании на C++, т.к. многие старожилы, узнав о CRTP, удивлённо поднимают брови – “Да я сам так много раз делал!”. Не зря, всё таки, оригинальный документ был озаглавлен как curiously recurring template pattern – эта техника действительно приходит многим в голову, без чтения каких либо статей или книг. Итак, пришло время рассмотреть способы применения CRTP в “боевых условиях”

Изгоняя виртуальность

Одним из способов применения CRPT, который очень часто упоминается, является “реализация” динамического полиморфизма, через статический.

Конечно, ни о какой полноценной замене виртуальных функций речи не идёт. Это скорее интересный метод замены ВФ, который применим в весьма специфичных условиях.

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

template<typename Derived>
class CrtpBase
{
public:
    void increment()
    {
        self()->incrementImpl();
    }
    void decrement()
    {
        self()->decrementImpl();
    }
    void doubleCounter()
    {
        self()->doubleCounterImpl();
    }
    void halfCounter()
    {
        self()->halfCounterImpl();
    }
    void print()
    {
        self()->printImpl();
    }
private:
    Derived* self()
    {
        return static_cast<Derived*>(this);
    }
};

Здесь мы видим, что CrtpBase не содержит никаких членов и просто вызывает методы класса, которым он будет параметризован при инстанциации. При этом используется “хитрый трюк”, для получения указателя  на класс-наследник(Derived). Всё, что описано в вышеприведённом классе возможно только тогда, когда следующий допущения справедливы:

  • Derived содержит все используемые в реализации интерфейса методы, и они доступны в CrtpBase.
  • Derived является наследником CrtpBase

    В противном случае мы получим ошибку компиляции.

    “Да это же одна из реализаций шаблона проектирования “Стратегия”!” – подумал знающий читатель. И, наверное, он прав. Но, всё же есть небольшие отличия от “канонической” формы – с CRTP мы не создаём накладных расходов. CrtpBase не занимает ничего в Derived, в то время как “Стратегия”, всё же, подразумевает композицию. Хотя это уже вопрос трактовок и того, что считать канонической формой.

    Теперь рассмотрим то, как будет выглядеть класс-наследник:

    class CrtpImpl : public CrtpBase<CrtpImpl>
    {
        friend class CrtpBase<CrtpImpl>;
    private:
        void incrementImpl()
        {
            m_Counter++;
        }
        void decrementImpl()
        {
            m_Counter--;
        }
        void doubleCounterImpl()
        {
            m_Counter *= 2;
        }
        void halfCounterImpl()
        {
            m_Counter /= 2;
        }
        void printImpl()
        {
            std::cout << "Crtp Impl counter: " << m_Counter << "\n";
        }
    private:
        int m_Counter = 0;
    };

    Так как интерфейс мы наследуем из CrtpBase, то и засорять публичный интерфейс деталями реализации негоже. Именно поэтому мы используем friend. Хотя все наверняка знают, что настоящие ООПшники не используют friend, т.к. “это зло и вообще убивает инкапсуляцию” – мы не будет стеснять себя в средствах. У нас нет цели порадовать ООПшников, у нас цель решить задачу, по возможности элегантно.

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

    Итак, почему реализация с CRTP может быть быстрее таковой с ВФ:

  • CRTP позволяет встраивание(inline), да не просто позволяет, CRTP его фактически навязывает. C ВФ всё не так просто.
  • Для вызова ВФ необходимо считать адрес из таблицы ВФ, и только после этого вызывать. У нас же обращение прямое.

    Все это, безусловно, делает CRTP быстрее ВФ. Или не делает? Есть только один способ убедится в нашей теории – измерить!

    Используем вышеуказанные классы и добавим вариант с ВФ. Код получился несколько длинным, но достаточно простым; весь код содержится в одном файле:

    #include <numeric>
    #include <vector>
    #include <algorithm>
    #include <chrono>
    #include <iostream>
    #include <string>
    #include <ctime>  
    
    class VirtualBase
    {
    public:
        virtual void increment() = 0;
        virtual void decrement() = 0;
        virtual void doubleCounter() = 0;
        virtual void halfCounter() = 0;
        virtual void print() = 0;
    };
    
    class VirtualImpl: public VirtualBase
    {
    public:
        void increment() override
        {
            m_Counter++;
        }
        void decrement() override
        {
            m_Counter--;
        }
        void doubleCounter() override
        {
            m_Counter *= 2;
        }
        void halfCounter() override
        {
            m_Counter /= 2;
        }
        void print()
        {
            std::cout << "Virtual Impl counter: " << m_Counter << "\n";
        }
    private:
        int m_Counter = 0;
    };
    
    template<typename Derived>
    class CrtpBase
    {
    public:
        void increment()
        {
            self()->incrementImpl();
        }
        void decrement()
        {
            self()->decrementImpl();
        }
        void doubleCounter()
        {
            self()->doubleCounterImpl();
        }
        void halfCounter()
        {
            self()->halfCounterImpl();
        }
        void print()
        {
            self()->printImpl();
        }
    private:
        Derived* self()
        {
            return static_cast<Derived*>(this);
        }
    };
    
    class CrtpImpl: public CrtpBase<CrtpImpl>
    {
        friend class CrtpBase<CrtpImpl>;
    private:
        void incrementImpl()
        {
            m_Counter++;
        }
        void decrementImpl()
        {
            m_Counter--;
        }
        void doubleCounterImpl()
        {
            m_Counter *= 2;
        }
        void halfCounterImpl()
        {
            m_Counter /= 2;
        }
        void printImpl()
        {
            std::cout << "Crtp Impl counter: " << m_Counter << "\n";
        }
    private:
        int m_Counter = 0;
    };
    
    template<typename Impl>
    void executeCrtp(CrtpBase<Impl>& crtp,
        const std::vector<unsigned>& indicies)
    {
        for(auto index : indicies)
        {
            switch(index)
            {
            case 0:
                crtp.increment();
                break;
            case 1:
                crtp.decrement();
                break;
            case 2:
                crtp.doubleCounter();
                break;
            case 3:
                crtp.halfCounter();
                break;
            }
        }
    }
    
    void executeVirtual(VirtualBase& virt,
        const std::vector<unsigned>& indicies)
    {
        for(auto index : indicies)
        {
            switch(index)
            {
            case 0:
                virt.increment();
                break;
            case 1:
                virt.decrement();
                break;
            case 2:
                virt.doubleCounter();
                break;
            case 3:
                virt.halfCounter();
                break;
            }
        }
    }
    
    int main()
    {
        std::vector<unsigned> indicies(10000000);
        unsigned counter = 0;
        std::generate(std::begin(indicies), std::end(indicies),
            [&counter]{return counter++ % 4; });
        std::srand(static_cast<unsigned>(std::time(0)));
        std::random_shuffle(std::begin(indicies), std::end(indicies));
    
        CrtpImpl crtpImpl;
        VirtualImpl virtImpl;
    
        auto start = std::chrono::high_resolution_clock::now();
        executeCrtp(crtpImpl, indicies);
        auto crtpElapsed = (std::chrono::high_resolution_clock::now() - start).count();
        std::cout << "CRTP time elapsed: " << crtpElapsed << "\n";
        start = std::chrono::high_resolution_clock::now();
        executeVirtual(virtImpl, indicies);
        auto virtElapsed = (std::chrono::high_resolution_clock::now() - start).count();
        std::cout << "Virtual time elapsed: " << virtElapsed << "\n";
        float ratio = std::max<float>(virtElapsed, crtpElapsed)/
            std::min<float>(virtElapsed, crtpElapsed);
        std::string relation = crtpElapsed < virtElapsed ? 
            std::string("faster") : std::string("slower");
        std::cout << "CRTP is " << ratio << " " << relation << "!\n";
        //Force implementation to not optimize out crtpImpl&virtImpl objects
        crtpImpl.print();
        virtImpl.print();
        std::cout << "CRTP size: " << sizeof(CrtpImpl) << "\n";
        std::cout << "Virtual size: " << sizeof(VirtualImpl) << "\n";
        return 0;
    }
    

    Как вы можете видеть мы используем 10 миллионов вызовов различных методов объектов CrtpImpl и VirtImpl. Введение элемента случайности, большое количество вызовов и использование разных методов служат одной цели: нивелировать влияние предсказателя ветвлений(branch predictor). Так как мы используем один массив индексов функций, то можно говорить о некой честности, при анализе производительности двух различных методов вызовов функций. Давайте посмотрим на результаты:

    MSVC2013

    GCC 4.9

    Clang(Apple LLVM version 5.0)

    CRTP time elapsed: 630157
    Virtual time elapsed: 570133
    CRTP is 1.10528 slower!
    Crtp Impl counter: -26613
    Virtual Impl counter: -26613
    CRTP size: 4
    Virtual size: 16

    CRTP time elapsed: 70539291
    Virtual time elapsed: 76674468
    CRTP is 1.08698 faster!
    Crtp Impl counter: 3142498
    Virtual Impl counter: 3142498
    CRTP size: 4
    Virtual size: 16

    CRTP time elapsed: 87236368
    Virtual time elapsed: 108518577
    CRTP is 1.24396 faster!
    Crtp Impl counter: -147204
    Virtual Impl counter: -147204
    CRTP size: 4
    Virtual size: 16

    Ну как, впечатляют результаты? Я думаю,что не очень. CRTP оказался медленнее на MSVC, быстрее на 8% при использование gcc(Ubuntu 14.04), и имеет преимущество в 24% при использование clang(OS X 10.8). Не густо, прямо скажем. Надо сказать, что результат довольно стабильный на gcc и clang, в то время как на MSVC он скачет то в плюс, то в минус. Поэтому разницу между ВФ и CRTP в gcc и MSVC можно считать погрешностью – CRTP и ВФ имеют одинаковую производительность в данном тесте, на данных компиляторах. Почему так происходит? Я не изучал код, сгенерированный gcc, но MSVC провёл девиртуализацию и встроил все виртуальные функции точно так же, как он поступил и с шаблонным классом. Разницы в сгенерированном коде нет никакой. Я полагаю, что gcc сделал тоже самое. А это значит, что наше предположение, о преимуществах шаблонов перед ВФ, за счёт встраивания, не верно! По крайней мере в данном, конкретном случае.

    Что же, пример банален и прост, но реальные программы сложнее. В реальных программах у нас много модулей, а не один. А много модулей может уже и не позволить встроить ВФ, с шаблонами же такой проблемы нет – они всегда полностью видны в каждом используемом модуле. Поэтому предлагаю разбить наш проект на модули, и проверить, что же мы получим в результате:

    Classes.h:

    #pragma once
    
    #include <iostream>
    #include <vector>
    
    class VirtualBase
    {
    public:
        virtual void increment() = 0;
        virtual void decrement() = 0;
        virtual void doubleCounter() = 0;
        virtual void halfCounter() = 0;
        virtual void print() = 0;
    };
    
    class VirtualImpl : public VirtualBase
    {
    public:
        void increment() override;
        void decrement() override;
        void doubleCounter() override;
        void halfCounter() override;
        void print();
    private:
        int m_Counter = 0;
    };
    
    template<typename Derived>
    class CrtpBase
    {
    public:
        void increment()
        {
            self()->incrementImpl();
        }
        void decrement()
        {
            self()->decrementImpl();
        }
        void doubleCounter()
        {
            self()->doubleCounterImpl();
        }
        void halfCounter()
        {
            self()->halfCounterImpl();
        }
        void print()
        {
            self()->printImpl();
        }
    private:
        Derived* self()
        {
            return static_cast<Derived*>(this);
        }
    };
    
    class CrtpImpl : public CrtpBase<CrtpImpl>
    {
        friend class CrtpBase<CrtpImpl>;
    private:
        void incrementImpl()
        {
            m_Counter++;
        }
        void decrementImpl()
        {
            m_Counter--;
        }
        void doubleCounterImpl()
        {
            m_Counter *= 2;
        }
        void halfCounterImpl()
        {
            m_Counter /= 2;
        }
        void printImpl()
        {
            std::cout << "Crtp Impl counter: " << m_Counter << "\n";
        }
    private:
        int m_Counter = 0;
    };
    
    template<typename Impl>
    void executeCrtp(CrtpBase<Impl>& crtp,
        const std::vector<unsigned>& indicies)
    {
        for(auto index : indicies)
        {
            switch(index)
            {
            case 0:
                crtp.increment();
                break;
            case 1:
                crtp.decrement();
                break;
            case 2:
                crtp.doubleCounter();
                break;
            case 3:
                crtp.halfCounter();
                break;
            }
        }
    }
    
    void executeVirtual(VirtualBase& virt, const std::vector<unsigned>& indicies);

    Classes.cpp:

    #include "Classes.h"
    
    void VirtualImpl::increment()
    {
        m_Counter++;
    }
    void VirtualImpl::decrement()
    {
        m_Counter--;
    }
    void VirtualImpl::doubleCounter()
    {
        m_Counter *= 2;
    }
    void VirtualImpl::halfCounter()
    {
        m_Counter /= 2;
    }
    void VirtualImpl::print()
    {
        std::cout << "Virtual Impl counter: " << m_Counter << "\n";
    }
    
    void executeVirtual(VirtualBase& virt,
        const std::vector<unsigned>& indicies)
    {
        for(auto index : indicies)
        {
            switch(index)
            {
            case 0:
                virt.increment();
                break;
            case 1:
                virt.decrement();
                break;
            case 2:
                virt.doubleCounter();
                break;
            case 3:
                virt.halfCounter();
                break;
            }
        }
    }

    main.cpp:

    #include <numeric>
    #include <vector>
    #include <algorithm>
    #include <chrono>
    #include <iostream>
    #include <string>
    #include <ctime>  
    #include "Classes.h"
    
    int main()
    {
        std::vector<unsigned> indicies(10000000);
        unsigned counter = 0;
        std::generate(std::begin(indicies), std::end(indicies),
            [&counter]{return counter++ % 4; });
        std::srand(static_cast<unsigned>(std::time(0)));
        std::random_shuffle(std::begin(indicies), std::end(indicies));
    
        CrtpImpl crtpImpl;
        VirtualImpl virtImpl;
    
        auto start = std::chrono::high_resolution_clock::now();
        executeCrtp(crtpImpl, indicies);
        auto crtpElapsed = (std::chrono::high_resolution_clock::now() - start).count();
        std::cout << "CRTP time elapsed: " << crtpElapsed << "\n";
        start = std::chrono::high_resolution_clock::now();
        executeVirtual(virtImpl, indicies);
        auto virtElapsed = (std::chrono::high_resolution_clock::now() - start).count();
        std::cout << "Virtual time elapsed: " << virtElapsed << "\n";
        float ratio = std::max<float>(virtElapsed, crtpElapsed)/
            std::min<float>(virtElapsed, crtpElapsed);
        std::string relation = crtpElapsed < virtElapsed ? 
            std::string("faster") : std::string("slower");
        std::cout << "CRTP is " << ratio << " " << relation << "!\n";
        //Force implementation to not optimize out crtpImpl&virtImpl objects
        crtpImpl.print();
        virtImpl.print();
        std::cout << "CRTP size: " << sizeof(CrtpImpl) << "\n";
        std::cout << "Virtual size: " << sizeof(VirtualImpl) << "\n";
    
        return 0;
    }
    

    Как вы можете видеть, мы вынесли часть кода в отдельный cpp, теперь на входе линковщика будут два объектных файла: main.obj и Classes.obj. Теперь то CRTP покажет свою силу!

    MSVC2013

    GCC 4.9

    Clang(Apple LLVM version 5.0)

    CRTP time elapsed: 639996
    Virtual time elapsed: 570268
    CRTP is 1.12227 slower!
    Crtp Impl counter: 2
    Virtual Impl counter: 2
    CRTP size: 4
    Virtual size: 16

    CRTP time elapsed: 69916442
    Virtual time elapsed: 75035399
    CRTP is 1.07322 faster!
    Crtp Impl counter: 36944663
    Virtual Impl counter: 36944663
    CRTP size: 4
    Virtual size: 16

    CRTP time elapsed: 92507645
    Virtual time elapsed: 119262093
    CRTP is 1.28921 faster!
    Crtp Impl counter: -147204
    Virtual Impl counter: -147204
    CRTP size: 4
    Virtual size: 16

    Мда, опять мимо… Новые результаты мало отличаются от предыдущих. Более того, любые отличия от предыдущих результатов можно списать на погрешность. А это значит, что никакой разницы просто-напросто нет!

    На MSVC очень легко получить разницу между первым и вторым вариантом, просто уберите переключатель /GL у компилятора.

    Как можно трактовать полученные результаты? Очень просто: компиляторы(и линковщики) гораздо умнее, чем многие программисты себе представляют. Вообще говоря, у меня складывается впечатление, что многие, из тех не многих, кто интересуется оптимизациями до сих пор свято верят, что компиляторы тупы и им надо помогать. Помогать надо, естественно, используя ассемблер и различные “хитрые трюки”. Но это далеко не так,- оптимизация является краеугольным камнем любого современного компилятора, поэтому все компиляторы постоянно совершенствуются на этом поприще. Поэтому оба наши предположения оказались неверны. И всё из-за того, что мы не учли способность к девиртуализации и меж-модульному анализу современных компиляторов. Что ещё раз доказывает уже набившую оскомину истину: прежде чем что-то оптимизировать – измерь! Интуиция и логика, в оптимизации, сталкиваясь с “суровой” реальностью, как правило, оказываются неверны!.

    Мне могут возразить: “А что если в момент вызова неизвестен тип, скрывающийся за указателем\ссылкой? Тогда компилятор не сможет применить ни ту, ни другую оптимизацию!”. Я соглашусь, но как тогда, в данном случае, может быть использован CRTP? CRTP это, по определению, решение, которое зиждется на том факте, что компилятор знает тип в точке вызова. CRTP не является полноценной заменой ВФ и никогда ей являться не будет: динамическое связывание не реализуется посредством статического – это разные сущности.

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

    Смешивание типов

    Хоть я и начал повествование о CRTP со сравнения оного с ВФ, первоначальный документ, посвящённый CRTP, был совсем о другом. Да и в “природе” CRTP встречается куда чаще с совершенно иной целью, не имеющей никакого отношения к [мнимой] производительности. Настоящая красота CRTP раскрывается в т.к. называемом смешивание(mixin) типов. Под этим подразумевается ситуация, когда мы добавляем функционал базового класса в наш класс-наследник, который использует знание о наследнике. Это позволяет не просто наследовать интерфейс, а наследовать реализацию, которая будет специфичная именно нашему классу. Это очень полезная и широко применимая техника, которую мы и рассмотрим в рамках данного параграфа. Чтобы лучше понять, как элегантно можно решить проблему, с использованием CRTP, предлагаю рассмотреть несколько примеров, которые и раскроют суть т.н. “смешивания”.

    Boost.Operators

    Boost.Operators является великолепной иллюстрацией  применения CRTP: реализуя один из операторов и унаследовав один из классов представленных в Operators мы получаем весь спектр необходимых операторов. Например:

    #include <cassert>
    #include <boost/operators.hpp>
    
    class Number: boost::less_than_comparable<Number>,
        boost::equivalent<Number>
    {
    public:
        Number(int value):
            m_Value{value}
        {}
        bool operator<(const Number& other) const
        {
            return m_Value < other.m_Value;
        }
    private:
        int m_Value;
    };
    
    int main()
    {
        Number one{1};
        Number two{2};
        Number three{3};
        Number four{4};
        assert(one >= one);
        assert(three <= four);
        assert(two == two);
        assert(three > two);
        assert(one < two);
        return 0;
    }
    

    Таким образом, определив один оператор < мы получили весь спектр операторов сравнения! Я думаю, что если вы поняли суть CRTP, то реализация boost::less_than_comparable<Number> и boost::equivalent<Number> не должна быть для вас загадкой. Всё же я приведу возможную реализацию Equivalent:

    template<class Derived>
    struct Equivalent
    {
        bool operator==(const Derived& other)
        {
            Derived* self = static_cast<Derived*>(this);
            return !(*self < other) && !(other < *self);
        }
    };
    
    class Number: boost::less_than_comparable<Number>,
        public Equivalent<Number>
    {
    public:
        Number(int value):
            m_Value{value}
        {}
        bool operator<(const Number& other) const
        {
            return m_Value < other.m_Value;
        }
    private:
        int m_Value;
    };

    Конечно, реализация в boost несколько интереснее. Но, всё равно, довольно простая. Если кому интересно просто посмотрите соответствующий заголовок. К примеру, одно очень важное отличие в том, что вы можете наследовать любой из Operators приватно и всё будет работать как часы, в отличии от моей, менее элегантной реализации. Но суть остаётся той же. Как можно заметить из примера выше, мы именно смешиваем 2 типа: базовый и наследующий, в результате получая некий симбиоз, обладающий большими возможностями по сравнению с каждый из источников. 

    enable_share_from_this

    Еще одним великолепным примером применения CRTP является std::enable_shred_from_this. Я уже писал о нём, поэтому заострять внимания на том, как он применяется я не буду. Давайте просто посмотрим как его можно реализовать:

    template<typename T>
    class EnableSharedFromThis
    {
    public:
        std::shared_ptr<T> shared_from_this()
        {
            return std::shared_ptr<T>{m_Weak};
        }
        //...
    private:
        std::weak_ptr<T> m_Weak;
    };

    Как вы можете заметить, здесь используется знание о типе потомка для создания и возврата корректного std::shared_ptr – вполне типичное применение CRTP.

    Приведённый выше класс, естественно, не покрывает весь функционал std::enable_from_this. К примеру, если вы создаёте shared_ptr из класса-наследника enable_from_this, то вы получите корректное поведение. В то время как с использованием моего класса, вы, вероятнее всего, получите падение – всё дело в том, что std::make_shared, а так же конструктор shared_ptr, используют знания о enable_from_this для “особого” создания объектов классов, которые от него наследуются.

    Инициализатор

    Итак мы рассмотрели два прекрасных примера использования CRTP наследуя функционал. Теперь предлагаю зайти с другого конца и подмешать функционал  “снизу”, т.е. мы не будем менять класс, наследуя его от какой-то CRTP сущности. Вместо этого мы будем “декорировать” наш объект с помощью класса, использующего CRTP технику.

    Многие программисты сталкивались со следующей проблемой: предположим у нас есть базовый класс, в котором определена функция инициализации.  При этом, мы обязуем всех, кто наследует наш класс вызывать функцию инициализации. Но как это сделать? Как заставить пользователя не забыть вызвать функцию инициализации? Т.к. функция виртуальная, мы не можем вызывать её в конструкторе родителя. Эту проблему мы и попытаемся решить с помощью CRPT.

    Так вот выглядит наша иерархия:

    class Base
    {
    public:
        virtual void init() = 0;
    };
    
    class Testee: public Base
    {
    public:
        Testee()
        {
            std::cout << "Default ctor\n";
        }
        Testee(int i)
        {
            std::cout << "Complex ctor\n";
        }
        Testee(int i, int j)
        {
            std::cout << "Even more complex ctor\n";
        }
        void init() override
        {
            std::cout << "Initialized\n";
        }
    };

    А вот так будет выглядеть наш класс-помощник:

    template<typename T>
    class Initializer: public T
    {
    public:
        template<typename... Args>
        Initializer(Args&&... arg) : T(std::forward<Args>(arg)...)
        {
            this->init();
        }
    };

    И его использование:

    int main()
    {
        Initializer<Testee> test1{};
        Initializer<Testee> test2{1};
        Initializer<Testee> test3{1, 2};
        return 0;
    }

    Теперь пользователь не должен помнить о том, что он обязан вызвать функцию инициализации! Правда он теперь должен помнить, что он должен создавать объекты только через Initializer<T>. Но это , на мой взгляд, куда лучше чем помнить о вызове метода. Более того, мы можем пойти дальше и сделать все конструкторы в Testee protected, тогда пользователь точно не сможет забыть, что он должен создавать объект Testee посредством Initializer<T>.

    Таким образом мы убили двух зайцев: мы избавили пользователя от необходимости явно вызвать функции инициализации и мы, по-сути, вызвали корректную виртуальную функцию в конструкторе. Правда для этого пришлось добавить лишнюю сущность. Можно пойти ещё дальше и, добавив особой шаблонной магии, сделать Initializer<T> весьма общим – пусть старается определить какая функция инициализации есть у T, и, в зависимости от этого, вызывает корректную(естественно это будет работать с узким набором имён)

    Заключение

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

    Конечно, я мог бы привести еще массу примеров, как CRPT облегчает жизнь программистов, но я не думаю, что в этом есть смысл, т.к. каждый новый пример чем-то схож с предыдущим. Главное понимать суть. Более того, я уверен, что многие из вас могут привести примеров не меньше,- просто кто-то раньше не знал, что у подобной техники есть закреплённое название. Кстати, одним из ярких примеров применения CRTP являются библиотеки ATL/WTL, которые им буквально пронизаны насквозь.