Pull to refresh
721.69
OTUS
Цифровые навыки от ведущих экспертов

C++: чем CRTP лучше Шаблонного Метода?

Reading time4 min
Views6.8K
Original author: Shao Voon Wong

Введение

Паттерн Шаблонный Метод (Template Method), описанный в книге по паттернам проектирования за авторством “банды четырех” (GoF), не связан с шаблонами (templates) C++ и является поведенческим шаблоном. Curiously Recurring Template Pattern (CRTP или “странно повторяющийся шаблон”) является усовершенствованием паттерна Шаблонный Метод и представляет собой идиому C++, в которой класс X наследуется от реализации шаблонного класса, используя сам X в качестве шаблонного аргумента. Название этой идиоме было дано Джимом Коплиеном (Jim Coplien), который наблюдал ее в самых первых образцах шаблонного кода C++. Эта методика позволяет достигнуть эффекта, аналогичного использованию виртуальных функций, без накладных расходов (и некоторой гибкости) динамического полиморфизма. CRTP можно использовать вместо Шаблонного Метода при условии, что вам не нужен динамический полиморфизм во время выполнения. Этот паттерн широко используется в библиотеках Windows ATL и WTL.

Шаблонный Метод

Давайте сначала рассмотрим классический паттерн Шаблонный Метод. В своей работе Шаблонный Метод полагается на полиморфизм и, как вы могли догадаться из названия, шаблонный метод. В нашем примере абстрактный базовый класс AbstractTextout имеет 11 перегрузок функции Print и одну чисто виртуальную функцию Process, которая будет реализована только в производных классах. Среди возможных примеров полезных производных классов можно выделить логирование, вывод на консоль и вывод отладочной информации. В этом руководстве мы ограничимся реализацией класса для вывода отладочной информации.

class AbstractTextout
{
public:
    void Print(const wchar_t* fmt);
    // ... плюс еще 10 других перегруженных Print с разным количеством аргументов Box
protected:
    virtual void Process(const std::wstring& str) = 0;
};

Ниже вы можете наблюдать код одной из функций Print. Аргумент Box отвечает за преобразование POD (plain old data) в строку. Отличие от остальных перегрузок функции Print заключается в том, что они просто набивают больше аргументов Box в vs. Я не буду вдаваться в подробности реализации класса Box, так как цель этой статьи заключается в другом. Вы можете посмотреть ее в полном коде примера, ссылки на который приведены в конце статьи.

void AbstractTextout::Print(const wchar_t* fmt, Box D1)
{
    std::wstring wsfmtstr = fmt;

    std::vector<std::wstring> vs;
    vs.push_back( D1.ToString() );

    std::wstring str = StrReplace( wsfmtstr, vs );

    Process(str); // реализуется только в производном классе.
}

Вот как производный класс DebugPrint реализует функцию Process в DebugPrint.cpp:

void Process(const std::wstring& str)
{
#ifdef _DEBUG
    OutputDebugStringW( str.c_str() );
#endif
}

А так мы взаимодействуем с классом DebugPrint:

#include "DebugPrint.h"

void main()
{
    DebugPrint dp;

    dp.Print(L"Product:{0}, Qty:{1}, Price is ${2}\n", L"Shampoo", 1200, 2.65);
    // выводит "Product:Shampoo, Qty:1200, Price is $2.650000"
}

Для класса с виртуальными функциями создается специальная виртуальная таблица (vtbl). И конечно же наличие виртуальной таблицы подразумевает накладные расходы, связанные с определением правильной функции для вызова. Curiously Recurring Template Pattern же использует статический полиморфизм, который устраняет необходимость в этих накладных расходах. В следующем разделе мы разберемся, за счет чего это достигается.

Curiously Recurring Template Pattern

AbstractTextout теперь является шаблонным классом, что означает, что весь код, определенный в cpp, должен быть перемещен в заголовочный файл. Прежде чем вызывать Process, код сначала приводит (cast) себя к производному типу.

template <typename Derived> 
class AbstractTextout
{
public:
    void Print(const wchar_t* fmt, Box D1 )
    {
        std::wstring wsfmtstr = fmt;

        std::vector<std::wstring> vs;
        vs.push_back( D1.ToString() );

        std::wstring str = StrReplace( wsfmtstr, vs );

        static_cast<Derived*>(this)->Process(str);
    }
}

DebugPrint останется неизменным, за исключением того, что теперь сам является шаблонным типом в своем базовом классе AbstractTextout, и мы должны сделать функцию Process не виртуальной и переместить ее в заголовочный файл:

class DebugPrint : public AbstractTextout<DebugPrint>
{
public:
    void Process(const std::wstring& str)
    {
#ifdef _DEBUG
        OutputDebugStringW( str.c_str() );
#endif
    }
};

Использовать класс DebugPrint мы будем точно так же, как и раньше:

#include "DebugPrint.h"

void main()
{
    DebugPrint dp;

    dp.Print(L"Product:{0}, Qty:{1}, Price is ${2}\n", L"Shampoo", 1200, 2.65);
    // выводит "Product:Shampoo, Qty:1200, Price is $2.650000"
}

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

Полезные ссылки 

Лицензия

Эта статья вместе со всем исходным кодом и файлами находится под лицензией Code Project Open License (CPOL)

Код примеров

Tags:
Hubs:
Total votes 8: ↑5 and ↓3+3
Comments9

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS