Программирование шаблонов – это мощная встроенная функция C++, которую большинство разработчиков не уделяет достаточно времени для полного понимания, в основном потому, что она "просто работает".Однако важно знать тип шаблона. процесс вывода, поскольку выбор неправильного поведения вывода может вызвать непреднамеренные побочные эффекты, такие как вызов неверных перегрузок функций или приведение к ошибочным типам. Это может привести к снижению производительности или, что еще хуже, к неопределенному поведению.
Хотя есть несколько веток SO, которые неплохо объясняют, большинство онлайн-руководств по шаблонам кратки и расплывчаты. Следовательно, я пишу это как простое для понимания руководство по всему, что вы знаете, чтобы убедиться, что ваши шаблоны имеют правильный вывод типа.
Знакомство
template<class T> void Func(T& t) { // Do Func things } int a = 0; Func(a); // What is T deduced as here?
Давайте взглянем на пример функции шаблона выше. Func()
принимает ссылку на T
. Если мы передаем ему переменную a
типа int
, T
выводится как int
, а тип параметра функции становится int&
.
Просто, верно? В этот момент большинство разработчиков развернулись бы и продолжили бы свой день — и это понятно. Может показаться, что T
— это просто тип передаваемого аргумента. Однако это не всегда так.
template<class T> void Func(T&& t) { // Do Func<T> things } int a = 0; Func(a); // Now what is T?
Обратите внимание, что тип параметра для Func()
изменился на T&&
. В этом случае и T
, и тип параметра будут int&
. Да, выведенный тип является ссылкой.
Типы параметров шаблонной функции
Острый глаз среди нас, возможно, уже заметил, что наряду с T
функция шаблона тип параметра играет важную роль в том, какой тип поведения вывода будет использоваться.
При написании шаблонов мы, очевидно, не контролируем типы аргументов, которые будут переданы в наши шаблоны (если мы не ожидаем определенных типов, практикуйте SFINAE!). Следовательно, чтобы наш компилятор вывел правильные типы, мы должны выбрать правильный тип параметра функции.
Давайте представим фрагмент псевдокода, чтобы помочь понять мою точку зрения.
// Template function taking parameter type - ParamType template<class Type> void Func(ParamType param); Func(arg); // arg - the argument passed in // Three arguments we'll be passing in int a = 0; // int const int ca = a; // const int const int& cra = a; // reference to const int
Как показано выше, Type
,ParamType
и arg
представляют выведенный тип, тип параметра шаблонной функции и аргумент соответственно.
template <class T> void Foo(T& t); // ParamType of a pointer or a reference template <class T> void Foo(T t); // ParamType of neither a pointer nor reference (copy) template <class T> void Foo(T&& t); // ParamType of a universal reference
Мы можем разделить ParamType
на три категории: ParamType
для указателя или ссылки, ParamType
ни для указателя, ни ссылки. и ParamType
универсальной (/forwarding) ссылки.
ParamType — это указатель или ссылка.
template<class T> void Func(T& param); // ParamType is a reference
Как видно из нашего первого примера, мы знаем, что это самый простой случай. Тип, полученный для Type
, по существу является типом arg
.
int a = 0; Func(a); // Type is int, ParamType is int&
Например, если мы вызываем Func()
с arg
типа int
. Type
будет выведено как int
, а ParamType
станет int&
.
const int ca = a; Func(ca); // Type is const int, ParamType is const int&
Если мы заменим arg
переменной типа const int
, Type
будет выведено как const int
, а ParamType
станет const int&
.
const int& cra = ca; Func(cra); // Type is const int, ParamType is const int&
И если мы передаем arg
ссылочноготипа, мы получаем то же поведение, что и в предыдущих примерах, только с игнорированием его ссылочной принадлежности . Type
выводится как const int
, а ParamType
становится const int&
.
Следовательно, для шаблонных функций, где ParamType
является указателем или ссылкой, происходит следующий вывод типа:
Сопоставление с образцом более заметно, когда ParamType
содержит дополнительные квалификаторы. Например:
template<class T> void Func(const T& param); // ParamType is a reference-to-const int a = 0 Func(a); // Type is int, ParamType is const int& const int ca = a; Func(ca); // Type is int, ParamType is const int&
Если мы добавим к ParamType
квалификатор const
, сделав его ссылкой на const
, const
-ность arg
больше не должна быть частью Type
.
Этот процесс аналогичен, еслиParamType
является указателем, как показано ниже.
template<class T> void Func(T* param); int a = 0; Func(&a) // Type is int, ParamType is int* const int* pa = &a; Func(pa) // Type is const int, ParamType is const int*
ParamType не является указателем или ссылкой
Как и в любой функции с параметром, который не является ни указателем, ни ссылкой, мы передаем аргумент по значению. Подобно тому, как когда ParamType
не является указателем или ссылкой, аргумент, переданный в нашу функцию, копируется, то есть мы можем изменить нашу собственную копию. Это отражается в типе вычета, который имеет место.
template<class T> void Func(T param); // ParamType is neither a pointer nor reference int a = 0; // int Func(a); // Type is int, ParamType is int const int ca = a; // const int Func(ca); // Type is int, ParamType is int const int& cra = ca; // reference to const int Func(cra); // Type is int, ParamType is int
Как видно, квалификаторы и референтность аргумента полностью игнорируются. Это ожидаемо, потому что мы имеем дело с новой независимой копией исходного объекта, который был передан в качестве аргумента.
Например, передача аргумента const int
соответствует Type
, а ParamType
выводится как int
. То же самое касается передачи аргумента const int&
.
Таким образом, мы можем изобразить поведение для типа параметра функции шаблона, который не является ни указателем, ни ссылкой, следующим образом.
Для полноты, если указатель const
на тип const
передается в качестве аргумента. Единственное, что здесь копируется по значению, — это сам указатель. Это означает, что const
-ность объекта, на который он указывает, сохраняется.
template<class T> void Func(T param); // ParamType is neither a pointer nor reference int a = 0; const int* const cpa = &a; Func(cpa); // Type is const int*, ParamType is const int*
ParamType — это универсальная ссылка (ссылка на пересылку).
Этот тип, вероятно, является наиболее запутанным, поскольку синтаксис для универсального (или официально известного теперь как forwarding)reference параметра шаблона похож на функция, которая принимает ссылку на r-значение (T&&
). Следовательно, тип дедукции может не соответствовать ожиданиям большинства людей.
template<class T> void Func(T&& param); // ParamType is a universal reference int a = 0; Func(a); // Type is int&, ParamType is int&
Да, вы правильно поняли, выведенный тип действительно является ссылкой.
Чтобы понять это поведение, я рекомендую сосредоточиться на самом названии — универсальной ссылке. «универсальный» предполагает возможность получать l-value или r-value и различать их. Давайте рассмотрим несколько примеров.
const int ca = a; Func(ca); // Type is const int&, ParamType is const int& const int& cra = ca; Func(cra); // Type is const int&, ParamType is const int&
Если мы передаем l-значениеconst int
, T
и ParamType
выводятся как const int&
. Поведение аналогично, когда мы передаем ему ссылку. Таким образом, если аргумент представляет собой l-значение, ParamType
и T
выводятся как ссылка на l-значение.
// Passing in an r-value Func(42); // Type is int, ParamType is int&&
Если мы передаем r-значениеint
, ParamType
выводится как int&&
(ссылка на r-значение), а T
выводится как int
.
Таким образом, процесс вывода типа шаблона из универсального ссылочного типа параметра можно представить как таковой.
Однако важно отметить, что этот ParamType
— единственный тип, способный вывести тип reference
. Обычно это используется с помощью шаблонов для достижения идеальной переадресации —что вкратце означает сохранение rvalue-несовности аргумента с использованием std::forward
, поэтому правильная перегрузка функции (если тот, который принимает r-valueссылки существуют) будет вызван. Я планирую вскоре написать отдельную статью о семантике перемещения — о том, как работают std::move и std::forward, так что следите за новостями. А пока вот пример:
template <class T> class MyClass // Template class { public: // C-tor with perfect forwarding template<class U> MyClass(U&& v): myClassVar(std::forward<U>(v)) {} private: T myClassVar; }; int a = 7; MyClass<int> myClass1(a); // Forwards l-value to myClassVar's ctor MyClass<int> myClass2(42) // Forwards r-value to myClassVar's ctor
Еда на вынос
Программирование шаблонов — это мощный инструмент, который обязательно должен быть в наборе инструментов любого программиста на C++. Но, как и любой мощный инструмент, владелец должен знать, как им пользоваться, чтобы полностью раскрыть его потенциал. Для разработчика C++ это значит не отказываться от функций как «просто волшебства» :)
Надеюсь, эта короткая статья помогла вам лучше понять шаблоны C++! Если в статье что-то отсутствует или неясно, не стесняйтесь оставлять комментарии. Удачного кодирования!
Рио.
Полезные ресурсы: