Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Специализация шаблонов

Шаблон описывает общий случай. Иногда для конкретного типа нужен другой алгоритм – или общий вариант просто не работает. Специализация позволяет переопределить шаблон для отдельных аргументов, не трогая остальные.

Полная специализация

Задаём все параметры конкретно:

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: стандарт прямо разрешает специализировать его шаблоны для пользовательских типов. Добавлять туда новые функции или классы – неопределённое поведение.