В C++20 библиотечный шаблон принимает объект как нетиповой параметр: какие свойства типа делают такой объект допустимым и однозначно идентифицируют специализацию?
Объектный нетиповой параметр в C++20 допустим, если его тип является структурным типом. Для пользовательского типа это литеральный класс, у которого все базовые классы и нестатические поля открыты, не изменяемы через mutable, а их типы сами являются структурными типами либо массивами таких типов.
Эти ограничения позволяют компилятору сравнивать аргументы шаблона по структуре и значениям подобъектов, а не вызывать пользовательский operator==. Поэтому эквивалентные значения обозначают одну и ту же специализацию шаблона.
До C++20 нетиповые параметры в основном ограничивались значениями вроде целых чисел, перечислений, указателей и ссылок. Передать произвольный объект, например набор символов как часть параметров шаблона, было нельзя или требовало обходных решений через типы.
C++20 расширил набор допустимых аргументов объектами классов. Возникла принципиальная проблема: компилятору нужно надежно определить, когда два объекта обозначают одну специализацию, не полагаясь на произвольную пользовательскую логику сравнения и скрытое состояние объекта.
Понятие структурного типа решает эту проблему заранее заданными правилами сравнения состава объекта.
Рассмотрим шаблон, специализация которого должна зависеть от конфигурации, строки или другого небольшого значения. Значение вычисляется во время компиляции, но его тип может содержать закрытые поля, изменяемое состояние или нестандартное сравнение.
Если разрешить такие типы без ограничений, компилятор не получил бы универсального и предсказуемого способа установить эквивалентность аргументов. Это могло бы приводить к неоднозначной идентичности специализаций, зависимости от поведения operator== или к невозможности использовать значение как часть имени специализации.
Неверный выбор решения обычно приводит к попытке передать в шаблон обычный класс, например строковый объект. Такой код не пройдет проверку требований к структурному типу, даже если объект создан как constexpr.
Для пользовательского класса должны выполняться следующие условия:
public;mutable;К структурным типам также относятся скалярные типы и lvalue-ссылки. Для объектного параметра дополнительно требуется, чтобы переданное значение было допустимым константным выражением требуемого типа.
Tag<4> является структурным типом: поле открытое, неизменяемое через mutable, а char — структурный скалярный тип. При инстанцировании создается специальный объект параметра шаблона, представляющий значение abc внутри шаблона.
Эквивалентность определяется рекурсивно по подобъектам. Она не основана на адресе объекта и не требует пользовательского operator==. Поэтому добавление собственного оператора сравнения не превращает неподходящий класс в структурный и не меняет правила идентификации специализации.
Ограничения являются компромиссом. Они исключают многие удобные классы стандартной библиотеки, но взамен дают компилятору стабильные правила для формирования и сравнения специализаций. Если значение нельзя представить структурным типом, альтернативами остаются типовой параметр, перечисление, целочисленный идентификатор или передача данных во время выполнения.
В библиотеке маршрутизации нужно кодировать короткие имена маршрутов в типах, чтобы разные имена выбирали разные реализации на этапе компиляции. Использование обычной строки как нетипового параметра не подходит: строка содержит сложное внутреннее состояние и не является структурным типом.
Возможны три подхода. Целочисленные идентификаторы просты и эффективны, но требуют глобального ручного сопоставления имени и числа. Типовой параметр с отдельным типом для каждого имени дает надежную идентичность, но заставляет создавать много вспомогательных типов. Передача строки во время выполнения гибче, но исключает выбор реализации на этапе компиляции.
Практичным решением становится небольшой структурный wrapper над массивом char, как Tag в примере. Имя хранится непосредственно в параметре шаблона, специализации сравниваются по символам, а библиотека сохраняет статический выбор реализации без ручных числовых идентификаторов.
constexpr, чтобы передать его в шаблон?Нет. constexpr гарантирует возможность константного вычисления объекта, но не делает его тип структурным. Тип также должен удовлетворять требованиям к литеральному классу, доступности и типам всех подобъектов. Например, constexpr-объект класса с закрытым нестатическим полем все равно нельзя использовать как такой нетиповой аргумент.
operator== совпадение специализаций?Нет. Для структурного типа идентичность аргументов шаблона определяется специальными правилами эквивалентности шаблонных аргументов и рекурсивно учитывает подобъекты. Пользовательский оператор сравнения не является механизмом выбора специализации и не может разрешить нарушение требований к структурному типу.
mutable или закрытыми полями?mutable означает, что логически константный объект может иметь изменяемое состояние, поэтому его представление нельзя надежно рассматривать как неизменный набор параметров шаблона. Закрытые поля затрудняют фиксированное структурное правило сравнения и позволяют классу скрывать состояние, влияющее на смысл значения.
Вместо этого состояние следует вынести в открытые неизменяемые поля структурного wrapper-типа либо передавать его во время выполнения. Такой дизайн делает состав значения явным для компилятора и сохраняет предсказуемую идентичность специализаций.