Специализация шаблонов и Tag Dispatch
Овладейте полной и частичной специализацией, узнайте, когда предпочесть перегрузку, и найдите отправку тегов и если constexpr в качестве альтернативы для ветвления компиляции времени.
Зачем специализироваться?
Основной шаблон обеспечивает общую реализацию, которая работает для большинства типов. Но иногда конкретный тип нуждается в принципиально ином лечении. Например, std::vector - это специализация, которая упаковывает валки в биты для эффективности пространства (хотя это широко считается ошибкой в стандартной библиотеке).
Специализация шаблона позволяет предоставить альтернативное определение для конкретных аргументов шаблона. Есть два вида:
1. Полная (явная) специализация — вы фиксируете ** все параметры шаблона на конкретные типы.
2.Частичная специализация — вы фиксируете некоторые* параметры или ограничиваете их (например, специализируетесь на всех типах указателей). Частично специализация доступна только для шаблонов классов, а не для шаблонов функций.
Полная специализация
Полная специализация обеспечивает полностью настраиваемую реализацию для одного конкретного набора шаблонных аргументов. Синтаксис использует template <> (пустой список параметров), за которым следует класс или функция с конкретными аргументами.
#include <iostream>
#include <cstring>
// Primary template
template <typename T>
class Formatter {
public:
static std::string format(const T& value) {
return std::to_string(value);
}
};
// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
static std::string format(const char* value) {
return std::string("\"" ) + value + "\"";
}
};
// Full specialization for bool
template <>
class Formatter<bool> {
public:
static std::string format(bool value) {
return value ? "true" : "false";
}
};
int main() {
std::cout << Formatter<int>::format(42) << '\n'; // 42
std::cout << Formatter<const char*>::format("hi") << '\n'; // "hi"
std::cout << Formatter<bool>::format(true) << '\n'; // true
}Частная специализация
Частичная специализация ограничивает некоторые параметры шаблона, оставляя другие общие. Это чрезвычайно мощно для сопоставления семейств типов — всех указателей, всех массивов, всех пар и т. Д. Только класс / структура / переменные шаблоны поддерживают частичную специализацию.
#include <iostream>
#include <string>
#include <typeinfo>
// Primary template
template <typename T>
struct TypeInfo {
static std::string name() { return "general type"; }
};
// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
static std::string name() {
return "pointer to " + TypeInfo<T>::name();
}
};
// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
static std::string name() {
return "reference to " + TypeInfo<T>::name();
}
};
// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
static std::string name() {
return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
}
};
int main() {
std::cout << TypeInfo<int>::name() << '\n'; // general type
std::cout << TypeInfo<int*>::name() << '\n'; // pointer to general type
std::cout << TypeInfo<int**>::name() << '\n'; // pointer to pointer to general type
std::cout << TypeInfo<int[5]>::name() << '\n'; // array of 5 general type
}Функциональные шаблоны не могут быть частично специализированными — только полностью специализированными. Хуже того, специализации шаблонов функций не участвуют в разрешении перегрузки так, как вы могли бы ожидать. Компилятор сначала выбирает лучший шаблон base с помощью разрешения перегрузки, а затем проверяет, имеет ли этот базовый шаблон соответствующую специализацию.
Это приводит к удивительному поведению:
template <typename T> void f(T) { /* #1 - primary */ }
template <typename T> void f(T*) { /* #2 - overload for pointers */ }
template <> void f<int*>(int*) { /* #3 - specialization of #1 */ }Звонок f((int*)nullptr) выбирает #2 (лучшее совпадение перегрузки), а не #3 (который специализируется на #1, а не #2). Исправление: Используйте обычную перегрузку функций вместо специализации шаблона функций. Если вам нужно ветвление типа компиляции, используйте if constexpr или концепции.
Tag Паттерн диспетчеризации
Tag dispatch использует пустые типы структур («теги») для выбора перегрузок во время компиляции. Стандартная библиотека широко использует это — std::advance отправляет сообщения на тегах категорий итераторов. Это был доминирующий метод ветвления компиляции перед if constexpr.
#include <iostream>
#include <iterator>
#include <vector>
#include <list>
namespace detail {
// Tags are empty structs used purely for overload selection
template <typename Iter>
void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
it += n; // O(1) — random access
std::cout << "random access advance\n";
}
template <typename Iter>
void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
// O(n) — step one at a time, forward or backward
while (n > 0) { ++it; --n; }
while (n < 0) { --it; ++n; }
std::cout << "bidirectional advance\n";
}
template <typename Iter>
void advance_impl(Iter& it, int n, std::input_iterator_tag) {
while (n > 0) { ++it; --n; }
std::cout << "input iterator advance\n";
}
}
template <typename Iter>
void my_advance(Iter& it, int n) {
// iterator_traits extracts the category tag
detail::advance_impl(it, n,
typename std::iterator_traits<Iter>::iterator_category{});
}
int main() {
std::vector<int> v{1, 2, 3, 4, 5};
auto vit = v.begin();
my_advance(vit, 3); // random access advance
std::list<int> l{1, 2, 3, 4, 5};
auto lit = l.begin();
my_advance(lit, 3); // bidirectional advance
}Оригинальное название: The Modern Alternative
C++17 if constexpr обеспечивает более чистый способ ветвления во время компиляции в рамках одной функции. Выброшенная ветвь не инстанцируется, поэтому ее не нужно компилировать под данный тип. Это в значительной степени заменяет отправку тегов и SFINAE для простых случаев.
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>
template <typename T>
std::string stringify(const T& value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return '"' + value + '"';
} else if constexpr (std::is_same_v<T, const char*>) {
return std::string("\"" ) + value + "\"";
} else {
return "[non-printable]";
}
}
int main() {
std::cout << stringify(42) << '\n'; // 42
std::cout << stringify(3.14) << '\n'; // 3.140000
std::cout << stringify(std::string("hello")) << '\n'; // "hello"
std::cout << stringify("world") << '\n'; // "world"
}Используйте это руководство для ветвления времени компиляции:
if constexpr (C++17) — простейшая, работает в рамках одной функции, никаких особых шаблонов не требуется. Предпочитаю это для простой логики, основанной на типе.
Концепции/требования (C++20) — лучше всего для ограничения перегрузок функций или целых шаблонов. Самодокументирование и предоставление четких сообщений об ошибках.
Отправка тегов — все еще полезна, когда у вас есть существующая иерархия тегов (например, категории итераторов). Кодовые базы Pre-C++17 в значительной степени зависят от этого.
SFINAE/enable_if — самая старая техника. Труднее читать, загадочные ошибки. Избегайте в новом коде, если концепции или if constexpr работают.
Полная специализация — используйте шаблоны классов, когда вам нужна совершенно другая реализация для одного конкретного типа.
Никогда не используйте специализацию шаблона функций — вместо этого используйте перегрузку.
- Полная специализация (
template <>) обеспечивает индивидуальную реализацию для одного конкретного типа. - Часть специализации ограничивает некоторые параметры — доступны только для шаблонов классов, а не для шаблонов функций.
- Специализация шаблона функций подвержена ошибкам — предпочтительнее перегрузка или
if constexpr - Отправка тегов использует пустые типы структур для выбора перегрузок во время компиляции через
iterator_traits if constexpr(C++17) - это современная, более чистая альтернатива, которая отбрасывает незанятые ветви во время компиляции.
Тест — проверьте свои знания
(20 XP)1. Почему вы должны избегать специализированных шаблонов функций?
2. Какие типы шаблонов поддерживают частичную специализацию?
3. Чем `if constexpr` отличается от обычного `if`?