Программирование C++ШаблоныРазработчик библиотек на C++

Рассмотрите вывод аргументов шаблона класса для объекта, переданного в конструктор копирования: почему выво...

Рассмотрите вывод аргументов шаблона класса для объекта, переданного в конструктор копирования: почему вывод сохраняет исходную специализацию, а не вкладывает её параметр повторно?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

При выводе аргументов шаблона класса компилятор учитывает специальный кандидат вывода для копирования. Поэтому при передаче объекта уже созданной специализации, например Коробка<int>, вывод выбирает Коробка<int>, а не Коробка<Коробка<int>>.

Этот механизм предотвращает неожиданное повторное оборачивание типа при копировании объекта шаблонного класса.

Исторический контекст

До C++17 параметры шаблона класса обычно требовалось указывать явно при создании объекта. Class template argument deduction появился, чтобы разрешить вывод этих параметров из аргументов конструктора и сократить дублирование типов.

Без отдельного правила для копирования обычный конструктор, принимающий произвольный тип, мог бы интерпретировать копируемый объект как значение параметра шаблона. Это приводило бы к выводу вложенной специализации вместо сохранения исходного типа.

Постановка проблемы

Пусть шаблон класса имеет конструктор, принимающий значение произвольного типа. При создании нового объекта из существующего объекта шаблонного класса потенциально подходят два варианта: вывести параметр из типа аргумента конструктора либо распознать операцию как копирование той же специализации.

Если выбрать первый вариант без дополнительных правил, из Коробка<int> можно было бы получить Коробка<Коробка<int>>. Это меняет смысл операции копирования и может привести к неожиданному типу, несовместимости интерфейсов и ошибкам перегрузки.

Подробное решение

Для вывода аргументов шаблона класса компилятор формирует набор виртуальных функций-образцов на основе конструкторов и неявных руководств вывода. Среди них существует специальный кандидат вывода для копирующего или перемещающего конструктора.

Если аргумент имеет тип конкретной специализации шаблонного класса, этот кандидат предлагает сохранить ту же специализацию. При конкуренции с более общим конструктором копирующий кандидат имеет приоритет, поэтому параметр не выводится повторно как сам объект.

#include <iostream> template<class T> struct Коробка { Коробка(T) {} }; int main() { Коробка первая(42); // Коробка<int> Коробка вторая(первая); // Коробка<int>, не Коробка<Коробка<int>> std::cout << sizeof(вторая); }

В первой строке параметр выводится из целого числа. Во второй строке специальный кандидат распознаёт аргумент как объект уже определённой специализации и сохраняет Коробка<int>.

Механизм относится именно к выводу аргументов шаблона класса, а не к обычному присваиванию и не к выводу параметров функции-шаблона. Если требуется другое поведение, его нужно выразить явным типом, отдельным конструктором или пользовательским руководством вывода. При этом руководство не должно случайно создавать более приоритетный и семантически неверный путь вывода.

Ситуация из практики

В библиотеке был контейнер-обёртка с универсальным конструктором. Разработчик ожидал, что копирование объекта Обёртка<int> создаст ещё одну Обёртка<int>, но отдельно проверил случай из-за наличия конструктора, принимающего произвольный тип.

Рассматривались три варианта. Явно указывать тип при каждом создании надёжно, но увеличивает дублирование. Добавлять пользовательское руководство вывода можно, но оно усложняет правила выбора и может конфликтовать с неявными кандидатами. Использовать стандартный кандидат копирования проще и корректнее, если конструктор действительно моделирует обычное копирование.

Выбран третий вариант: конструктор копирования не маскировался универсальным конструктором, а результат проверялся статическим утверждением типа. В итоге копирование сохраняло исходную специализацию, а универсальный конструктор применялся только к новым значениям.

Что кандидаты часто упускают

  1. Всегда ли копирование объекта шаблонного класса использует кандидат копирования?

Нет. Кандидат участвует именно в выводе параметров шаблона класса. После того как тип уже известен, выбор обычного конструктора выполняется по стандартным правилам перегрузки. Кроме того, пользовательские руководства вывода и доступность конструкторов могут изменить набор и приоритет кандидатов.

  1. Чем этот механизм отличается от обычного конструктора копирования?

Конструктор копирования создаёт объект уже известного типа. Кандидат копирования решает другую задачу: сначала определить специализацию шаблона класса по аргументу, а затем создать объект этой специализации. Поэтому он влияет на этап вывода типа, а не только на саму инициализацию.

  1. Можно ли ожидать такой же эффект при передаче объекта в функцию-шаблон?

Нет. Вывод параметров функции-шаблона подчиняется собственным правилам. Если параметр функции имеет форму, позволяющую вывести тип непосредственно из аргумента, будет выведен тип самого аргумента — например, Коробка<int>. Специальный кандидат копирования для CTAD в этом процессе не участвует.