Программирование шаблонов – это мощная встроенная функция 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++! Если в статье что-то отсутствует или неясно, не стесняйтесь оставлять комментарии. Удачного кодирования!

Рио.

Полезные ресурсы:

Эффективный современный C++ Скотта Мейерса (О’Рейли)

Справочник по шаблонам C++