В практической ситуации: почему при создании объекта шаблонного класса тип его параметра иногда нельзя вывести из аргументов конструктора, и какую роль в этом играют руководства вывода?
Вывод параметров шаблона класса анализирует аргументы конструктора, но только те параметры, которые можно однозначно связать с параметрами шаблона. Если параметр класса вообще не представлен в типах конструктора или связь недостаточна, автоматический вывод завершается ошибкой. Руководство вывода явно задаёт соответствие между аргументами создания объекта и специализацией шаблонного класса.
До C++17 параметры шаблонов классов обычно приходилось указывать явно, даже если они очевидно следовали из аргументов конструктора. Это создавало повторение типов и ухудшало читаемость обобщённого кода.
Class template argument deduction, или CTAD, появился в C++17, чтобы приблизить создание объектов шаблонных классов к вызову шаблонных функций. Компилятор строит неявные руководства вывода на основе конструкторов, а разработчик может добавить пользовательское руководство для более сложных случаев.
Рассмотрим класс, у которого конструктор принимает только размер, а тип хранимого значения задаётся отдельным параметром шаблона. По одному числу невозможно определить, должен ли объект хранить int, double или другой тип.
Если ошибочно ожидать вывода из будущего использования объекта или из значения по умолчанию поля, код не скомпилируется. CTAD не угадывает тип по контексту присваивания и не анализирует произвольные последующие операции с объектом.
Для каждого конструктора компилятор формирует неявное руководство вывода. Его параметры соответствуют параметрам конструктора, а результатом является специализация класса. Если параметры конструктора позволяют вывести все параметры шаблона, выбирается соответствующая специализация.
В примере конструктор принимает только std::size_t, поэтому T из него вывести нельзя. Пользовательское руководство сообщает: объект, созданный с таким аргументом, следует рассматривать как Box<int>.
Руководство вывода участвует в выборе специализации, но не является конструктором и не заменяет инициализацию. После выбора Box<int> компилятор проверяет, можно ли фактически вызвать конструктор этой специализации.
Для простых конструкторов обычно достаточно неявных руководств. Пользовательские руководства нужны, когда тип результата отличается от прямого типа аргумента: например, при выводе типа элемента из диапазона итераторов или при удалении служебного обёрточного типа.
Основное ограничение состоит в том, что руководство должно быть согласовано с реальным интерфейсом класса. Оно может сообщить компилятору нужную специализацию, но не может сделать некорректный конструктор вызываемым. При неоднозначных руководствах применяется обычное разрешение перегрузки, поэтому слишком общие руководства способны вызвать конфликт.
В библиотеке контейнеров требовалось создавать объект буфера по размеру, а тип элемента должен был быть заранее выбран политикой библиотеки. Попытка положиться только на CTAD не сработала: аргумент-размер не содержал информации о типе элемента.
Рассматривались два варианта. Явно писать специализацию при каждом создании было надёжно и прозрачно, но увеличивало повторение типов. Добавить конструктор с фиктивным аргументом типа позволяло использовать неявный вывод, однако ухудшало API и создавало лишний параметр.
Выбрали пользовательское руководство вывода для поддерживаемых политик. Оно сохранило чистый интерфейс конструктора и централизовало правило выбора специализации. Для неоднозначных случаев оставили явное указание параметра шаблона, чтобы ошибка не маскировалась неочевидным выбором.
1. Выводится ли параметр шаблона класса из типа переменной, которой присваивается созданный объект?
Нет. CTAD выполняется по аргументам непосредственного создания объекта и доступным руководствам вывода. Тип переменной слева обычно не используется как источник вывода параметров класса, поэтому ожидание, что присваивание автоматически выберет специализацию, является ошибочным.
2. Чем пользовательское руководство вывода отличается от конструктора?
Конструктор создаёт объект и проверяет возможность его инициализации. Руководство только описывает, какую специализацию класса выбрать по набору аргументов. После выбора специализации вызов конструктора всё равно должен быть корректным; добавление руководства не создаёт отсутствующий конструктор и не меняет правила времени выполнения.
3. Что произойдёт, если пользовательское руководство конфликтует с неявным?
Все подходящие руководства участвуют в разрешении перегрузки. Если одно из них выбирается однозначно, используется его результат; если несколько имеют одинаково подходящий приоритет, возникает неоднозначность компиляции. Поэтому пользовательское руководство следует делать достаточно точным и проверять его взаимодействие с конструкторами, включая копирующее руководство вывода.