Первичный шаблон и выбор реализации

Шаблон позволяет описать общее поведение для множества типов. Например, первичный шаблон Stack<T> хранит элементы в std::vector:

#include <cassert>
#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    void push(const T& value)
    {
        elems.push_back(value);
    }

    void pop()
    {
        assert(!elems.empty());
        elems.pop_back();
    }

    const T& top() const
    {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const
    {
        return elems.empty();
    }
};

Без специализаций этот шаблон применяется ко всем допустимым аргументам: например, Stack<int> и Stack<double>.

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

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

Полная специализация задаёт отдельную реализацию для конкретного набора аргументов шаблона. Определим Stack<std::string>, который использует std::deque вместо контейнера первичного шаблона:

#include <deque>
#include <string>

template<>
class Stack<std::string>
{
    std::deque<std::string> elems;

public:
    void push(const std::string& value);

    void pop()
    {
        assert(!elems.empty());
        elems.pop_back();
    }

    const std::string& top() const
    {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const
    {
        return elems.empty();
    }
};

Пустой список в template<> означает, что новых параметров шаблона нет: аргумент уже зафиксирован как std::string.

Полная специализация является самостоятельным классом. Она не получает поля и определения методов из первичного шаблона, поэтому нужный интерфейс необходимо объявить заново.

Метод полной специализации, определённый за пределами класса, оформляется как метод обычного класса - повторять template<> перед ним не нужно:

inline void Stack<std::string>::push(const std::string& value)
{
    elems.push_back(value);
}

inline позволяет разместить определение в заголовке, включаемом в несколько единиц трансляции. Если определение находится только в одном файле .cpp, спецификатор inline обычно не требуется.

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

Stack<std::string> strings;
strings.push("hello");

const std::string& value = strings.top();

Подробные правила приведены в разделах стандарта о явных специализациях и inline-функциях.

Полная специализация отдельного метода

Если изменить нужно только один член шаблона, весь класс специализировать необязательно. Можно создать полную, или явную, специализацию этого члена для конкретного аргумента.

#include <iostream>

template<typename T>
struct Printer
{
    void print(const T& value) const
    {
        std::cout << value;
    }
};

template<>
inline void Printer<bool>::print(const bool& value) const
{
    std::cout << (value ? "true" : "false");
}
Printer<int>{}.print(42);     // 42
Printer<bool>{}.print(true);  // true

Здесь специализируется только Printer<bool>::print(). Сам Printer<bool> по-прежнему создаётся из первичного шаблона, поэтому его остальные члены сохраняют исходные определения.

Синтаксис и ограничения описаны в правилах явной специализации шаблонов и их членов.

Частичная специализация для указателей

Частичная специализация описывает не один конкретный аргумент, а форму, которой должны соответствовать аргументы. Реализация Stack<T*> будет выбрана для любого указательного типа:

template<typename T>
class Stack<T*>
{
    std::vector<T*> elems;

public:
    void push(T* value)
    {
        elems.push_back(value);
    }

    T* pop();

    T* top() const
    {
        assert(!elems.empty());
        return elems.back();
    }

    bool empty() const
    {
        return elems.empty();
    }
};

При сопоставлении фактического аргумента с формой T* компилятор выводит параметр T:

Stack<int*> a;        // T = int
Stack<const int*> b;  // T = const int
Stack<int**> c;       // T = int*

При определении метода за пределами класса сохраняются и список параметров, и форма специализации Stack<T*>:

template<typename T>
T* Stack<T*>::pop()
{
    assert(!elems.empty());

    T* result = elems.back();
    elems.pop_back();

    return result;
}

Стек невладеющих указателей

int value = 42;

Stack<int*> pointers;
pointers.push(&value);

int* p = pointers.pop();
*p = 100; // value теперь равен 100

Удаление адреса из контейнера не уничтожает объект value. За время жизни объектов отвечает вызывающий код.

Поэтому в примере pop() возвращает указатель, хотя в первичном шаблоне он имеет тип void. Это допустимо, но обобщённый код, работающий с разными вариантами Stack, должен учитывать различие интерфейсов.

Подробнее см. правила сопоставления частичных специализаций и членов частичной специализации.

Специализации с несколькими параметрами

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

// Первичный шаблон
template<typename T1, typename T2>
struct MyClass
{
    static constexpr int id = 0;
};

// Одинаковые типы
template<typename T>
struct MyClass<T, T>
{
    static constexpr int id = 1;
};

// Второй аргумент равен int
template<typename T>
struct MyClass<T, int>
{
    static constexpr int id = 2;
};

// Два указателя
template<typename T1, typename T2>
struct MyClass<T1*, T2*>
{
    static constexpr int id = 3;
};

В специализации MyClass<T1*, T2*> параметры T1 и T2 выводятся независимо. Следующие варианты выбираются однозначно:

static_assert(MyClass<int, float>::id == 0, "primary");
static_assert(MyClass<float, float>::id == 1, "same types");
static_assert(MyClass<float, int>::id == 2, "second is int");
static_assert(MyClass<int*, float*>::id == 3, "two pointers");

Для MyClass<int, float> ни одна специализация не подходит, поэтому используется первичный шаблон. В остальных случаях применяется соответствующая частичная специализация.

Неоднозначность и её разрешение

Один набор аргументов может соответствовать сразу нескольким частичным специализациям:

// Неоднозначно: подходят MyClass<T, T> и MyClass<T, int>.
// MyClass<int, int> first;

// Неоднозначно: подходят MyClass<T, T> и MyClass<T1*, T2*>.
// MyClass<int*, int*> second;

Компилятор сравнивает подходящие варианты с помощью частичного упорядочения. Программа корректна, только если удаётся выбрать единственную наиболее специализированную реализацию. В показанных пересечениях исходные варианты не превосходят друг друга, поэтому выбор неоднозначен.

Полная специализация для конкретной пары

Пересечение для int, int можно разрешить полной специализацией:

template<>
struct MyClass<int, int>
{
    static constexpr int id = 4;
};

static_assert(MyClass<int, int>::id == 4, "full specialization");

Более узкая частичная специализация

Для двух одинаковых указательных типов можно добавить более специализированный вариант:

template<typename T>
struct MyClass<T*, T*>
{
    static constexpr int id = 5;
};

static_assert(MyClass<int*, int*>::id == 5, "same pointer types");
static_assert(MyClass<double*, double*>::id == 5, "same pointer types");

Форма MyClass<T*, T*> уже, чем условия «два одинаковых типа» и «два произвольных указателя». Поэтому для двух одинаковых указательных типов она становится единственным наиболее специализированным вариантом.

Алгоритм выбора описан в разделах стандарта о поиске подходящих частичных специализаций и частичном упорядочении.

Порядок объявлений

Сначала должен быть объявлен первичный шаблон. Только после этого можно объявлять его полные и частичные специализации.

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

Полная специализация должна быть объявлена до первого использования, которое привело бы к неявному созданию соответствующей специализации первичного шаблона. Это требование действует в каждой единице трансляции, где происходит такое использование.

Для полной специализации всего класса достаточно доступного объявления первичного шаблона. Если специализируется отдельный член, должно быть доступно определение шаблона класса.

template<typename T>
struct Value
{
    static constexpr int id = 0;
};

template<>
struct Value<int>
{
    static constexpr int id = 1;
};

static_assert(Value<int>::id == 1, "explicit specialization is visible");

Частичная специализация

Частичная специализация также должна быть объявлена до первого использования, для которого она должна участвовать в выборе реализации. Если в этой точке она не видна, программа не может полагаться на её более позднее объявление.

template<typename T>
struct Category
{
    static constexpr int id = 0;
};

template<typename T>
struct Category<T*>
{
    static constexpr int id = 1;
};

static_assert(Category<int*>::id == 1, "partial specialization is visible");

На практике первичный шаблон и предназначенные для общего использования специализации обычно помещают в один заголовок. Так во всех единицах трансляции сохраняются одинаковые объявления и одинаковый результат выбора.

Нормативные требования приведены отдельно для явных специализаций и частичных специализаций.

Что нужно запомнить

  • Первичный шаблон задаёт общее поведение.
  • Полная специализация фиксирует весь набор аргументов и определяет самостоятельный класс.
  • Отдельный член шаблона можно полностью специализировать, не заменяя весь класс.
  • Частичная специализация описывает семейство аргументов, например все указательные типы.
  • Члены частичной специализации не наследуются из первичного шаблона.
  • При нескольких подходящих частичных специализациях должна существовать единственная наиболее специализированная.
  • Специализации объявляют до использования, для которого они должны влиять на выбор реализации.
  • Шаблоны функций не поддерживают частичную специализацию; вместо неё обычно используют перегрузку.