
Введение
Паттерн Шаблонный Метод (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)