Разберите механизм template<auto V>: почему tag<42> и tag<42u> становятся разными специализациями?
#include <type_traits>
template<auto V>
struct tag {};
static_assert(!std::is_same_v<tag<42>, tag<42u>>);
auto в нетиповом параметре выводится отдельно для каждого аргумента. Для 42 выводится тип int, а для 42u — unsigned int, поэтому tag<42> и tag<42u> являются разными специализациями шаблона.
Значение нетипового параметра идентифицирует специализацию не само по себе: учитывается пара выведенный тип + значение.
До C++17 тип нетипового параметра требовалось явно указывать в объявлении шаблона, например template<int V>. Это ограничивало универсальность шаблонов, которым нужно принимать значения разных допустимых типов.
Появление template<auto V> позволило поручить компилятору вывод типа нетипового аргумента, сохранив проверку значения на этапе компиляции. Такой механизм удобен для обобщённых compile-time-таблиц, тегов и диспетчеризации по константам.
При использовании auto легко ошибочно считать, что одинаковые числовые значения всегда обозначают одну специализацию. Но литералы 42 и 42u имеют разные типы, а тип параметра входит в идентичность специализации.
Это влияет на перегрузки, специализации, выбор функций и совместимость типов. Если библиотека использует tag<V> как compile-time-идентификатор, незаметное изменение суффикса литерала может создать другой тип.
В объявлении template<auto V> компилятор выводит тип параметра из записанного нетипового аргумента. Литерал 42 имеет тип int, поэтому выражение tag<42> концептуально соответствует специализации с параметром int V = 42. Литерал 42u имеет тип unsigned int, поэтому tag<42u> соответствует другой специализации.
Для нетипового параметра важны следующие свойства:
Неявное преобразование не превращает эти две специализации в одну. Компилятор не выбирает общий тип вроде unsigned int только потому, что оба значения числовые: при auto тип выводится из конкретного аргумента.
Практическое следствие: если API должен нормализовать значения к одному типу, это нужно сделать явно. Например, вместо template<auto V> можно использовать template<std::size_t V> или передавать значение через фабрику, которая выполняет явное преобразование. Цена такой нормализации — потеря универсальности для других типов.
Следует также отличать тип нетипового параметра от типа выражения после некоторых преобразований. Для строковых литералов, указателей, ссылок и объектов действуют дополнительные правила допустимости и структурности; сам факт использования auto не делает любой объект допустимым нетиповым аргументом.
В библиотеке маршрутизации compile-time-обработчики индексируются через tag<код>. Один модуль регистрирует обработчик как tag<200>, а другой обращается к нему через tag<200u>. Числовое значение совпадает, но типы тегов различаются, поэтому специализации таблицы или перегрузки могут не совпасть.
Возможны два решения. Оставить template<auto V> и требовать единообразных суффиксов — это сохраняет универсальность, но делает ошибку использования менее заметной. Либо объявить template<std::uint16_t V>, явно задав доменный тип кода; это лучше фиксирует контракт, хотя ограничивает диапазон и допустимые формы аргументов.
Для кода состояния протокола выбран второй вариант: размер и смысл значения известны заранее, поэтому единый тип уменьшает риск появления несовместимых тегов. В результате 200 и 200u, если оба допустимы после проверки, приводятся к одной модели идентификатора.
Вопрос: Можно ли считать tag<42> и tag<42L> одинаковыми, если значения равны?
Ответ: Нет. 42 обычно имеет тип int, а 42L — long, поэтому auto выводит разные типы нетипового параметра. Следовательно, создаются разные специализации tag.
Вопрос: Можно ли передать строковый литерал напрямую в template<auto V>?
Ответ: Не в виде обычного строкового литерала как значения указателя: строковый литерал не является допустимым нетиповым аргументом указательного типа. В C++20 можно использовать подходы со структурным типом-обёрткой, если её тип удовлетворяет требованиям структурного типа; тогда в параметр передаётся объект, а не сам массив строкового литерала.
Вопрос: Что изменится, если объявить параметр как template<std::size_t V>?
Ответ: Тип параметра больше не выводится из аргумента: он зафиксирован как std::size_t. Значения, представимые этим типом и допустимые после требуемых преобразований, будут относиться к одной схеме специализаций. Это делает идентичность типов предсказуемее, но запрещает использовать значения других смысловых типов без явного преобразования и проверяет диапазон целевого типа.