Первичный шаблон и выбор реализации
Шаблон позволяет описать общее поведение для множества типов. Например, первичный шаблон 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");
На практике первичный шаблон и предназначенные для общего использования специализации обычно помещают в один заголовок. Так во всех единицах трансляции сохраняются одинаковые объявления и одинаковый результат выбора.
Нормативные требования приведены отдельно для явных специализаций и частичных специализаций.
Что нужно запомнить
- Первичный шаблон задаёт общее поведение.
- Полная специализация фиксирует весь набор аргументов и определяет самостоятельный класс.
- Отдельный член шаблона можно полностью специализировать, не заменяя весь класс.
- Частичная специализация описывает семейство аргументов, например все указательные типы.
- Члены частичной специализации не наследуются из первичного шаблона.
- При нескольких подходящих частичных специализациях должна существовать единственная наиболее специализированная.
- Специализации объявляют до использования, для которого они должны влиять на выбор реализации.
- Шаблоны функций не поддерживают частичную специализацию; вместо неё обычно используют перегрузку.