Как частичная специализация шаблонной переменной позволяет вычислить признак для типов с заданной структурой?
Шаблонная переменная может иметь первичное определение со значением по умолчанию и частичную специализацию для определённого семейства типов. При обращении к ней компилятор выбирает наиболее специализированное совпадающее определение, поэтому значение вычисляется во время компиляции.
До появления шаблонных переменных в C++14 метапрограммирование обычно оформляли через шаблонные классы с static constexpr-полем, например trait-классы. Такой подход остаётся полезным, но для простых значений требует обращаться к вложенному полю вроде trait<T>::value.
Шаблонные переменные сделали интерфейс признака более прямым: значение можно выразить как trait_v<T>. Это уменьшает шаблонный синтаксис и позволяет специализировать непосредственно переменную, а не только класс-обёртку.
Предположим, библиотеке нужно на этапе компиляции определить, является ли тип экземпляром определённого шаблона класса. Проверка должна возвращать false для произвольных типов и true для всех специализаций нужного семейства, включая разные типы элементов и параметры распределителя.
Неверное решение может проверять только конкретный тип или использовать перегрузки функций. Тогда результат становится менее удобным для применения в static_assert, ограничениях шаблонов и других контекстах, где требуется константное выражение.
Первичное определение шаблонной переменной задаёт общий случай. Частичная специализация сопоставляет параметры исходного шаблона с параметрами конкретного семейства типов. Если тип подходит под специализацию, используется её значение; иначе остаётся первичное определение.
Минимальный пример:
В специализации T и Allocator выводятся из проверяемого типа. Поэтому совпадают, например, std::vector<int> и std::vector<double, пользовательский_распределитель>, но не std::list<int>.
Это не перегрузка переменной: компилятор выполняет выбор специализации шаблона. Если подходят несколько частичных специализаций, применяются правила частичного упорядочивания; неоднозначность приводит к ошибке компиляции.
У решения есть ограничения. Частичная специализация для std::vector<T, Allocator> не совпадёт с const std::vector<T, Allocator>, поскольку квалификатор верхнего уровня входит в проверяемый тип. При необходимости тип сначала нормализуют, например применяя std::remove_cv_t или std::remove_cvref_t в отдельном интерфейсе.
Ключевое слово inline в примере относится к переменной C++17 и позволяет безопасно иметь одно и то же определение в нескольких единицах трансляции, например через заголовочный файл. Сам механизм частичной специализации от этого не меняется.
В библиотеке сериализации нужно выбрать оптимизированный путь для std::vector, но не для остальных контейнеров. Рассматривались три варианта: проверять тип через перегруженные функции, создать trait-класс со значением value или использовать шаблонную переменную.
Перегрузки функций удобны внутри вызова, но хуже подходят для static_assert и ограничений типов. Trait-класс совместим со старым кодом и может хранить дополнительные метаданные, однако для одного булева признака создаёт избыточную оболочку.
Выбран шаблон переменной с первичным значением false и частичной специализацией для std::vector. В результате выбор стратегии происходит на этапе компиляции, не создаёт runtime-проверки и читается как обычное константное свойство типа. Для поддержки const и ссылок интерфейс дополнительно нормализует входной тип перед обращением к признаку.
Да, специализация может менять и значение, и тип переменной, если это допускается выбранной формой объявления. Однако для trait-признаков обычно сохраняют один и тот же тип, например bool, чтобы результат предсказуемо использовался в static_assert и ограничениях.
Важно отличать частичную специализацию от перегрузки: переменные не перегружаются по параметрам. Компилятор выбирает одну специализацию шаблона по правилам сопоставления и частичного упорядочивания.
Если одна специализация однозначно более специализирована, будет выбрана она. Например, специализация для семейства std::vector может быть более узкой, чем специализация для любого шаблона с определённой формой параметров.
Если ни одна из подходящих специализаций не является более специализированной, выбор неоднозначен и программа не компилируется. Порядок объявления специализаций сам по себе не разрешает такую неоднозначность.
Шаблонная переменная хорошо подходит для одного простого значения. Но trait-класс может одновременно предоставить несколько связанных результатов: категорию типа, преобразованный тип, константные флаги и вспомогательные псевдонимы.
Кроме того, класс проще расширять дополнительными частичными специализациями и использовать как тип в других шаблонах. Поэтому шаблонная переменная часто служит удобным кратким интерфейсом поверх trait-класса, а не полностью заменяет его.