В чём состоит главный риск CTAD при использовании шаблонного класса в публичном интерфейсе?
CTAD (Class Template Argument Deduction) выводит параметры шаблонного класса из аргументов конструктора, но этот вывод может оказаться неожиданным для пользователя библиотеки. В публичном интерфейсе это опасно тем, что изменение конструктора или направляющих вывода способно изменить выводимый тип, поведение перегрузок и требования к времени жизни объектов. Поэтому CTAD удобен в локальном коде, а на границе API тип часто лучше указывать явно либо скрывать создание за фабрикой с чётким контрактом.
До C++17 параметры шаблонов класса обычно приходилось писать явно даже тогда, когда их можно было однозначно определить из аргументов конструктора. Это создавало избыточный синтаксис и ухудшало читаемость кода, особенно для пар, контейнероподобных типов и обёрток.
В C++17 появился CTAD: компилятор строит набор неявных направляющих вывода на основе конструкторов, выбирает подходящую направляющую и получает специализацию шаблонного класса. Позже механизм был дополнен пользовательскими направляющими вывода и поддержкой дополнительных случаев.
Рассмотрим шаблонную обёртку, конструктор которой принимает значение по значению. При передаче массива такой параметр подвергается стандартным преобразованиям, включая преобразование массива в указатель. В результате CTAD может выбрать специализацию с типом указателя, хотя разработчик ожидал владение строкой или копирование содержимого.
Для публичного API проблема шире одного опасного преобразования. Выводимый тип становится частью исходного кода пользователя: от него зависят перегрузки, доступные операции, время жизни ресурсов и иногда ABI. Добавление или изменение конструктора, а также новой направляющей вывода может сделать прежний вызов неоднозначным или привести к другой специализации.
При CTAD компилятор рассматривает конструкторы как неявные функции вывода. Для каждого кандидата он пытается вывести параметры шаблона из типов аргументов, затем выбирает наиболее подходящий кандидат обычными правилами разрешения перегрузок. После этого создаётся объект уже конкретной специализации; CTAD не является динамическим механизмом и не меняет тип объекта во время выполнения.
Например:
В первом объявлении массив при передаче параметру T v преобразуется в указатель, поэтому получается *Box<char>**. Такая обёртка не владеет массивом: если text выйдет из области видимости, указатель внутри a станет висячим. Во втором объявлении тип задан явно, и массив преобразуется в std::string, который хранит собственную копию.
На вывод влияют не только конструкторы, но и пользовательские направляющие вывода. Они позволяют задать библиотечную политику, например нормализовать определённые входные типы к безопасной специализации. Однако направляющая только определяет параметры шаблона; после её выбора компилятор всё равно должен найти подходящий конструктор специализации. Неправильно согласованная направляющая может привести к ошибке уже на этапе конструирования.
Внутри реализации CTAD обычно улучшает читаемость и уменьшает дублирование. На публичной границе предпочтительнее явно фиксировать тип, если он определяет владение, представление данных, требования к времени жизни или семантику интерфейса. Альтернативой служит именованная фабрика, которая скрывает детали шаблона и возвращает заранее определённый тип.
Команда разрабатывает библиотечную обёртку Box<T> для конфигурационных значений. В тестах разработчики используют CTAD с объектами std::string, поэтому всё работает ожидаемо. Затем клиент передаёт буфер char[], и обёртка выводит Box<char*>; после выхода буфера из области видимости чтение значения становится неопределённым поведением.
Рассматривались три варианта. Полный запрет CTAD устраняет неоднозначность, но делает локальный код более многословным. Пользовательская направляющая, переводящая строковые входы в Box<std::string>, сохраняет удобство, но требует тщательно проверить все формы инициализации и совместимость с конструкторами. Явное указание типа на публичных вызовах наиболее прозрачно, хотя увеличивает объём кода.
Выбрано сочетание: внутри реализации CTAD разрешён, а публичная фабрика принимает строковые представления и возвращает Box<std::string>. Это явно фиксирует владение и время жизни, не заставляя пользователей знать внутреннюю специализацию. В результате изменение конструкторов внутри библиотеки не меняет контракт клиентского кода.
Что произойдёт, если ни одна направляющая вывода не подходит?
Вывод параметров шаблона завершится ошибкой компиляции. Компилятор не будет угадывать специализацию по типу переменной слева или по предполагаемому назначению объекта: параметры должны быть выведены из инициализатора и доступных направляющих. Если нужное правило не выражается конструкторами, его можно добавить пользовательской направляющей либо указать специализацию явно.
Может ли пользовательская направляющая вывода сама сконструировать объект?
Нет. Направляющая является правилом только для выбора специализации, а не конструктором и не исполняемым кодом. После вывода, например T, компилятор пытается вызвать конструктор выбранного Class<T> с исходными аргументами. Поэтому направляющая и набор конструкторов должны быть согласованы: иначе вывод формально успешен, но создание объекта завершится ошибкой.
Почему добавление конструктора в шаблонный класс способно нарушить клиентский код с CTAD?
Конструктор может породить новую неявную направляющую вывода. Она способна стать лучшим кандидатом для прежнего набора аргументов, создать неоднозначность или вывести другую специализацию. Даже если интерфейс конструктора кажется обратно совместимым по смыслу, изменение фактического типа объекта влияет на перегрузки, требования к памяти и доступные операции. Для стабильного публичного API это ещё одна причина не полагаться на неявный вывод там, где тип является частью контракта.