Шаблон описывает общий случай. Иногда для конкретного типа нужен другой алгоритм – или общий вариант просто не работает. Специализация позволяет переопределить шаблон для отдельных аргументов, не трогая остальные.
Полная специализация
Задаём все параметры конкретно:
template <typename T>
struct Printer {
static void Print(const T& value) { std::cout << value; }
};
template <> // список параметров пуст
struct Printer<bool> { // аргумент задан явно
static void Print(bool value) { std::cout << (value ? "true" : "false"); }
};
Printer<int>::Print(42); // общий шаблон: 42
Printer<bool>::Print(true); // специализация: true
Специализация – независимый класс. Он не обязан иметь те же методы, что и общий шаблон, и не
наследует от него ничего. На практике интерфейс всё же выдерживают: иначе шаблонный код,
работающий через Printer<T>, сломается на одном конкретном T.
Специализировать можно и отдельный метод, не трогая класс:
template <>
void Printer<bool>::Print(const bool& value) { /* ... */ }
Частичная специализация
Задаём не конкретные аргументы, а их форму:
template <typename T>
struct Printer<T*> { // для любых указателей
static void Print(T* p) {
if (p) {
Printer<T>::Print(*p);
} else {
std::cout << "null";
}
}
};
template <typename K, typename V>
struct Printer<std::pair<K, V>> { // для любых пар
static void Print(const std::pair<K, V>& p) {
std::cout << '(';
Printer<K>::Print(p.first);
std::cout << ", ";
Printer<V>::Print(p.second);
std::cout << ')';
}
};
Компилятор выбирает наиболее специализированный подходящий вариант: для Printer<int*>
победит версия с T*, а не общая.
Частичная специализация существует только для шаблонов классов. У функций её нет – вместо неё работает обычная перегрузка, и это чаще всего удобнее: template <typename T> void Print(T*) – это перегрузка, а не специализация, и участвует в разрешении по обычным правилам.
Трейты
Самое частое применение специализации – не менять алгоритм, а сообщать факты о типе.
template <typename T>
struct TriviallyCopyable {
static const bool value = false; // консервативное умолчание
};
template <> struct TriviallyCopyable<char> { static const bool value = true; };
template <> struct TriviallyCopyable<int> { static const bool value = true; };
template <> struct TriviallyCopyable<double> { static const bool value = true; };
Умолчание выбрано false намеренно: неверный true даст испорченные данные, неверный false –
всего лишь потерю скорости.
Теперь алгоритм может спросить про тип и выбрать реализацию:
template <typename T>
void CopyN(T* dst, const T* src, size_t count) {
if (TriviallyCopyable<T>::value) {
std::memcpy(dst, src, count * sizeof(T));
} else {
for (size_t i = 0; i < count; ++i) {
dst[i] = src[i];
}
}
}
Условие вычисляется на этапе компиляции, и оптимизатор выбрасывает мёртвую ветвь целиком. С C++17
это записывают явно – if constexpr, и тогда невыбранная ветвь даже не проверяется на
корректность.
Выигрыш по сравнению со специализацией самой CopyN в том, что трейт переиспользуется: тот же
факт о типе пригодится любому другому алгоритму.
SameType своими руками
Классическое упражнение – шаблон, отвечающий, совпадают ли два типа:
template <typename T, typename U>
struct SameType {
static const bool value = false;
};
template <typename T>
struct SameType<T, T> { // частичная специализация: оба аргумента одинаковы
static const bool value = true;
};
struct Dummy {};
using Type = int;
SameType<int, int>::value; // true
SameType<int, Type>::value; // true: Type – псевдоним, а не новый тип
SameType<int, int&>::value; // false: ссылка – другой тип
SameType<int, const int>::value; // false: const – часть типа
SameType<Dummy, Dummy>::value; // true
Последние два случая – хорошая проверка понимания: const и & не «модификаторы поведения», а
часть типа, и на уровне системы типов различают всё.
Это ровно std::is_same из стандартной библиотеки.
Что уже есть в стандартной библиотеке
Заголовок <type_traits> содержит десятки готовых трейтов:
#include <type_traits>
std::is_same<T, U>::value;
std::is_pointer<T>::value;
std::is_integral<T>::value;
std::is_base_of<Base, Derived>::value;
std::is_trivially_copyable<T>::value; // тот самый, только настоящий
std::remove_const<T>::type; // трейт, возвращающий тип
std::remove_reference<T>::type;
С C++14 и C++17 у них появились короткие формы: std::is_same_v<T, U> вместо ::value и
std::remove_const_t<T> вместо ::type.
Многие трейты невозможно реализовать средствами языка – например, std::is_trivially_copyable требует поддержки компилятора. Именно поэтому свои трейты пишут только для собственных предметных свойств («этот тип умеет сериализоваться»), а всё про устройство типов берут готовым.
Специализация под свой тип
Стандартная библиотека сознательно оставляет точки расширения: специализируя её шаблоны для собственных типов, вы встраиваетесь в готовые механизмы.
struct Point {
int x;
int y;
bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};
namespace std {
template <>
struct hash<Point> { // теперь Point можно класть в unordered_map
size_t operator()(const Point& p) const {
return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
}
};
}
Это единственный законный повод что-то добавлять в пространство имён std: стандарт прямо
разрешает специализировать его шаблоны для пользовательских типов. Добавлять туда новые функции
или классы – неопределённое поведение.