16.5. Специализация шаблона
Не всегда можно написать один шаблон, который наилучшим образом подходит для всех возможных типов аргументов шаблона, для которых может быть создан его экземпляр. В некоторых случаях общий шаблон просто не подходит для типа: он либо приводит к ошибке при компиляции, либо к неправильным действиям. С другой стороны, иногда можно воспользоваться уникальными возможностями определенного типа для создания более эффективной функции, чем та, которой снабжен экземпляр общего шаблона.
Функция compare() — хороший пример шаблона функции, общее определение которого не подходит для специфического типа, а именно символьных указателей. Хотелось бы, чтобы функция compare() сравнивала символьные указатели, используя функцию strcmp(), а не сравнивала значения указателей. Действительно, ведь уже есть перегруженная функция compare(), обрабатывающая символьные строковые литералы (см. раздел 16.1.1):
// первая версия; может сравнить любые два типа
template <typename Т> int compare(const T&, const T&);
// вторая версия, для обработки строковых литералов
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
Однако версия функции compare() с двумя параметрами значения шаблона будет вызвана только при передаче строкового литерала или массива. Если происходит вызов функции compare() с символьными указателями, будет вызвана первая версия шаблона:
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // вызывает первый шаблон
compare("hi", "mom"); // вызывает шаблон с двумя параметрами значения
Нет никакого способа преобразовать указатель в ссылку на массив, поэтому вторая версия функции compare() не подходит для передачи указателей p1 и p2 как аргументов.
Для обработки символьных указателей (в отличие от массивов) можно определить специализацию шаблона (template specialization) для первой версии функции compare(). Специализация — это отдельное определение шаблона, в котором определяется один или несколько параметров шаблона для получения специфического типа.
Специализация шаблона функции
При специализации шаблона функции следует предоставить аргументы для каждого параметра первоначального шаблона. Для указания специализации шаблона используется ключевое слово template, сопровождаемое парой пустых угловых скобок (<>). Пустые скобки означают, что аргументы будут предоставлены для всех параметров первоначального шаблона:
// специальная версия compare() для работы с указателями на символьные
// массивы
template <>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
Трудная для понимания часть этой специализации относится к типам параметра функции. При определении специализации типы параметров функции должны совпадать с соответствующими типами ранее объявленного шаблона:
template <typename Т> int compare(const T&, const T&);
В этой специализации параметры функции являются ссылками на константные типы. Подобно псевдонимам типа, взаимодействие между типами параметра шаблона, указателями и константами может удивить (см. раздел 2.5.1).
Необходимо определить специализацию шаблона этой функции с типом const char* для параметра Т. Функция потребует ссылки на константную версию этого типа. Константная версия типа указателя — это константный указатель, а не указатель на константу (см. раздел 2.4.2). В данной специализации следует использовать тип const char* const &, являющийся ссылкой на константный указатель на константный символ.
Перегрузка функций или специализация шаблона
При определении специализации шаблона функции разработчик, по существу, выполняет задачу компилятора. Таким образом, определение предоставляется для использования специфического экземпляра первоначального шаблона. Важно понимать, что специализация — это создание экземпляра функции; а не перегрузка ее экземпляра.
Может ли определение некой функции как специализации шаблона или как независимой, не шаблонной функции повлиять на подбор функций? Предположим, например, что имеется определение двух версий шаблонной функции compare(): той, что получает параметры как ссылки на массив, и другой, которая получает тип const T&. Факт наличия специализации для символьных указателей никак не влияет на подбор функции:
compare("hi", "mom")
Когда функция compare() вызывается для строкового литерала, оба шаблона функции оказываются подходящими и обеспечивают одинаково хорошее (т.е. точное) соответствие вызову. Однако версия с параметрами символьного массива более специализирована (см. раздел 16.3), она и выбирается для этого вызова.
Если бы была определена версия функции compare(), получающая указатели на символы, как простая, не шаблонная функция (а не как специализация шаблона), то этот вызов разрешится по-другому. В данном случае было бы три подходящих функции: эти два шаблона и не шаблонная версия указателя на символ. Все три одинаково хорошо подходят для этого вызова. Как уже упоминалось, когда нешаблонная функция обеспечивает одинаково хорошее соответствие с шаблонной, выбирается нешаблонная функция (см. раздел 16.3).
Ключевая концепция. Обычные правила области видимости относятся и к специализации
Чтобы специализировать шаблон, объявление его оригинала должно быть в области видимости. Кроме того, объявление специализации должно быть в области видимости перед любым кодом, использующим экземпляр шаблона.
Пропуск объявления обычных классов и функций найти очень просто — компилятор не сможет обработать такой код. Но при отсутствии объявления специализации компилятор обычно создает код, используя первоначальный шаблон. Поэтому ошибки в порядке объявления шаблона и его специализации довольно просто допустить, но очень трудно найти.
Использование специализации и экземпляра первоначального шаблона с тем же набором аргументов шаблона является ошибкой. Но компилятор вряд ли обнаружит эту ошибку.
Специализация шаблона класса
Кроме специализации шаблонов функций, вполне можно также специализировать шаблоны классов. В качестве примера определим специализацию библиотечного шаблона hash, который можно использовать для хранения объектов класса Sales_data в неупорядоченном контейнере. По умолчанию неупорядоченные контейнеры используют для организации своих элементов класс hash<key_type> (см. раздел 11.4). Чтобы использовать его с собственным типом данных, следует определить специализацию шаблона hash. Специализированный класс hash должен определять следующее.
• Перегруженный оператор вызова (см. раздел 14.8), возвращающий тип size_t и получающий объект типа ключа контейнера.
• Два члена-типа result_type и argument_type, соответствующие типу возвращаемого значения и типу аргумента оператора вызова.
• Стандартный конструктор и оператор присвоения копии, которые могут быть определены неявно (см. раздел 13.1.2).
Единственное осложнение в определении этой специализации класса hash состоит в том, что специализация шаблона должна быть в том же пространстве имен, в котором определяется первоначальный шаблон. Более подробная информация о пространствах имен приведена в разделе 18.2, а пока достаточно знать, что к пространству имен можно добавлять члены. Для этого следует сначала открыть пространство имен:
// открыть пространство имен std, чтобы можно было специализировать
// класс std::hash
namespace std {
} // закрыть пространство имен std; обратите внимание: никакой точки с
// запятой после закрывающей фигурной скобки
Любые определения, расположенные между открывающей и закрывающей фигурными скобками, будут частью пространства имен std.
Следующий код определяет специализацию класса hash для класса Sales_data:
// открыть пространство имен std, чтобы можно было специализировать
// класс std::hash
namespace std {
template <> // определение специализации с параметром
struct hash<Sales_data> // шаблона класса Sales_data
{
// тип, используемый для неупорядоченного контейнера hash, должен
// определять следующие типы
typedef size_t result_type;
typedef Sales_data argument_type; // по умолчанию этому типу
// требуется оператор ==
size_t operator()(const Sales_data& s) const;
// класс использует синтезируемые функции управления копированием
// и стандартный конструктор
};
size_t
hash<Sales_data>::operator()(const Sales_data& s) const {
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // закрыть пространство имен std; обратите внимание: никакой точки с
// запятой после закрывающей фигурной скобки
Определение hash<Sales_data> начинается с части template<>, означающей, что определяется полностью специализированный шаблон. Специализируемый шаблон называется hash, а специализированная версия — hash<Sales_data>. Члены класса следуют непосредственно из требований для специализации шаблона hash.
Подобно любым другим классам, специализируемые члены можно определить в классе или вне его, как это сделано здесь. Перегруженный оператор вызова должен определять хеш-функцию по значениям заданного типа. Эта функция обязана возвращать каждый раз тот же результат, когда она вызывается для данного значения. Хеш-функция практически всегда возвращает другой результат для не равных объектов.
Все сложности определения хорошей хеш-функции делегируем библиотеке. Библиотека определяет специализации класса hash для встроенных типов и для большинства библиотечных типов. Безымянный объект hash<string> используется для создания хеш-кода для переменной-члена bookNo, объект типа hash<unsigned> для создания хеш-кода из переменной-члена units_sold и объекта типа hash<double> для создания хеш-кода из переменной-члена revenue. Применение к этим результатам оператора исключающего ИЛИ (см. раздел 4.8) сформирует общий хеш-код для заданного объекта класса Sales_data.
Следует заметить, что хеш-функция определена для хеширования всех трех переменных-членов, чтобы она была совместима с определением оператора operator== класса Sales_data (см. раздел 14.3.1). По умолчанию неупорядоченные контейнеры используют специализацию хеша, соответствующую типу key_type, наряду с оператором равенства типа ключа.
С учетом того, что специализация находится в области видимости, она будет использоваться автоматически при использовании класса Sales_data как ключ в одном из этих контейнеров:
// использует hash<Sales_data> и оператор operator== класса Sales_data
// из раздела 14.3.1
unordered_multiset<Sales_data> SDset;
Поскольку hash<Sales_data> использует закрытые члены класса Sales_data, этот класс следует сделать другом класса Sales_data:
template <class T> class std::hash; // нужно для объявления
// дружественным
class Sales_data {
friend class std::hash<Sales_data>;
// другие члены, как прежде
};
Здесь указано, что специфический экземпляр hash<Sales_data> является дружественным. Поскольку данный экземпляр определяется в пространстве имен std, следует помнить, что этот тип хеша определяется в пространстве имен std. Следовательно, объявление friend относится к std::hash.
Частичная специализация шаблона класса
В отличие от шаблона функции, специализация шаблона класса не обязана предоставлять аргументы для каждого параметра шаблона. Можно определить некоторые из них, но не все.
Частичная специализация (partial specialization) шаблона класса сама является шаблоном. Пользователи должны предоставить аргументы для тех параметров шаблона, которые не затронуты специализацией.
Библиотечный тип remove_reference был представлен в разделе 16.2.3, он работает с серией специализаций:
// первоначальный, наиболее общий шаблон
template <class Т> struct remove_reference {
typedef T type;
};
// частичные специализации, которые будут использоваться для ссылок
// на l- и r-значения
template <class Т> struct remove_reference<T&> // ссылки на l-значение
{ typedef Т type; };
template <class T> struct remove_reference<T&&> // ссылки на r-значение
{ typedef T type; };
Первый шаблон определяет самую общую версию. Его экземпляр может быть создан с любым типом; он использует свой аргумент шаблона как тип для своего члена type. Следующие два класса — это частичные специализации первоначального шаблона.
Поскольку частичная специализация — это шаблон, начнем, как обычно, с определения параметров шаблона. Подобно любой другой специализации, у частичной специализации то же имя, что и у специализируемого шаблона. Список параметров специализации шаблона включает элементы для каждого параметра шаблона, тип которого не был определен полностью при частичной специализации. После имени класса располагаются аргументы для параметров специализируемого шаблона. Эти аргументы располагаются в угловых скобках после имени шаблона. Аргументы позиционально соответствуют параметрам первоначального шаблона.
Список параметров шаблона частичной специализации — это подмножество или специализация списка параметров первоначального шаблона. В данном случае у специализаций то же количество параметров, что и у первоначального шаблона. Но тип параметров в специализациях отличается от первоначального шаблона. Специализация будут использоваться для ссылок на типы l- и r-значений соответственно:
int i;
// decltype(42) - это int, используется первоначальный шаблон
remove_reference<decltype(42)>::type a;
// decltype(i) - это int&, используется первая (Т&) частичная
// специализация
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) - это int&&, используется вторая (т.е., T&&)
// частичная специализация
remove_reference<decltype(std::move(i))>::type c;
У всех трех переменных, a, b и с, тип int.
Специализация членов, но не класса
Вместо специализации всего шаблона можно специализировать только одну или несколько его функций-членов. Например, если Foo — это шаблон класса с членом Bar, можно специализировать только этот член:
template <typename Т> struct Foo {
Foo (const T &t = T()): mem(t) { }
void Bar() { /* ... */ }
T mem;
// другие члены класса Foo
};
template<> // специализация шаблона
void Foo<int>::Bar() // специализация члена Bar класса Foo<int>
{
// осуществить всю специализированную обработку, относящуюся к целым
// числам
}
Здесь специализируется только один член класса Foo<int>. Другие его члены предоставляются шаблоном Foo:
Foo<string> fs; // создает экземпляр Foo<string>::Foo()
fs.Bar(); // создает экземпляр Foo<string>::Bar()
Foo<int> fi; // создает экземпляр Foo<int>::Foo()
fi.Bar(); // использует специализацию Foo<int>::Bar()
При использовании шаблона Foo с любым типом, кроме int, члены экземпляра создаются, как обычно. При использовании шаблона Foo с типом int все члены экземпляра, кроме Bar, создаются, как обычно. Если использовать член Bar класса Foo<int>, то получится специализированное определение.
Упражнения раздела 16.5
Упражнение 16.62. Определите собственную версию класса hash<Sales_data> и контейнер unordered_multiset объектов класса Sales_data. Поместите в контейнер несколько транзакций и выведите его содержимое.
Упражнение 16.63. Определите шаблон функции для подсчета количества вхождений заданного значения в векторе. Проверьте программу, передав ей вектор значений типа double, вектор целых чисел и вектор строк.
Упражнение 16.64. Напишите специализированную версию шаблона из предыдущего упражнения для обработки вектора vector<const char*> и используйте ее в программе.
Упражнение 16.65. В разделе 16.3 были определены две перегруженных версии функции debug_rep(), одна из которых получает параметр типа const char*, а вторая — типа char*. Перепишите эти функции как специализации.
Упражнение 16.66. Каковы преимущества и недостатки перегрузки функций debug_rep() по сравнению с определением специализаций?
Упражнение 16.67. Повлияет ли определение этих специализаций на подбор функций debug_rep()? Почему?