16.2.5. Дедукция аргумента шаблона и ссылки
Чтобы лучше понять дедукцию типа, рассмотрим такой вызов функции где параметр функции p является ссылкой на параметр типа шаблона T:
template <typename Т> void f(Т &p);
Обратите внимание на два момента: здесь применяются обычные правила привязки ссылок; и спецификаторы const здесь нижнего уровня, а не верхнего.
Дедукция типа из параметров ссылки на l-значения функций
Когда параметр функции представляет собой обычную ссылку (l-значение) на параметр типа шаблона (т.е. имеющего форму T&), правила привязки гласят, что передавать можно только l-значения (например, переменная или выражение, возвращающее ссылочный тип). Этот аргумент может быть или не быть константным. Если аргумент будет константой, то тип Т будет выведен как константный:
template <typename Т> void f1(Т&); // аргумент должен быть l-значением
// вызовы f1() используют ссылочный тип аргумента как тип параметра
// шаблона
f1(i); // i - это int; параметр шаблона Т - это int
f1(ci); // ci - это const int; параметр шаблона Т - это const int
f1(5); // ошибка: аргумент ссылочного параметра
// должен быть l-значением
Если параметр функции имеет тип const Т&, обычные правила привязки гласят, что можно передать любой вид аргумента — объект (константный или нет), временный объект или литеральное значение. Когда сам параметр функции является константой, выведенный для параметра Т тип не будет константным типом. Константность является частью типа параметра функции, и поэтому она не становится также частью типа параметра шаблона:
template <typename Т> void f2(const T&); // может получать r-значения
// параметр в f2() - это const &; const в аргументе неуместен
// в каждом из этих трех вызовов параметр функции f2() выводится
// как const int&
f2(i); // i - это int; параметр шаблона Т - это int
f2(ci); // ci - это const int, но параметр шаблона T - это int
f2(5); // параметр const & может быть привязан к r-значению;
// Т - это int
Дедукция типа из параметров ссылки на r-значения функций
Когда параметр функции является ссылкой на r-значение (см. раздел 13.6.1), т.е. имеет форму Т&&, обычные правила привязки гласят, что этому параметру можно передать r-значение. При этом дедукция типа ведет себя таким же образом, как дедукция обычного ссылочного параметра функции на l-значение. Выведенный тип для параметра Т — это тип r-значения:
template <typename Т> void f3(T&&);
f3(42); // аргумент - это r-значение типа int; параметр
// шаблона Т - это int
Сворачивание ссылок и параметры ссылок на r-значения
Предположим, что i является объектом типа int. Можно подумать, что такой вызов, как f3(i), будет недопустим. В конце концов, i — это l-значение, а ссылку на r-значение обычно нельзя связать с l-значением. Однако язык определяет два исключения из обычных правил привязки, которые позволяют это. На этих исключениях из правил основан принцип работы таких библиотечных функций, как move().
Первое исключение относится к дедукции типа для ссылочного параметра на r-значение. Когда l-значение (например, i) передается параметру функции, являющемуся ссылкой на r-значение на параметр типа шаблона (например, Т&&), компилятор выводит параметр типа шаблона как тип ссылки на l-значение аргумента. Поэтому, когда происходит вызов f3(i), компилятор выводит тип Т как int&, а не int.
Выведение типа Т как int&, казалось бы, означает, что параметр функции f3() будет ссылкой на r-значение типа int&. Обычно нельзя (непосредственно) определить ссылку на ссылку (см. раздел 2.3.1). Но это можно сделать косвенно, через псевдоним типа (см. раздел 2.5.1) или через параметр типа шаблона.
• X& &, X& && и X&& & сворачиваются в тип X&.
• Тип X&& && сворачивается в тип X&&.
Комбинация правил свертывания ссылок и специального правила дедукции типа для ссылочных на r-значения параметров означает, что можно вызвать функцию f3() для l-значения. Когда параметру функции f3() (ссылке на r-значение) передается l-значение, компилятор выведет тип T как тип ссылки на l-значение:
f3(i); // аргумент - это l-значение; параметр Т шаблона - это int&
f3(ci); // аргумент - это l-значение;
// параметр Т шаблона - это const int&
Когда параметр T шаблона выводится как ссылочный тип, правило свертывания гласит, что параметр функции T&& сворачивается в тип ссылки на l-значение. Например, результирующий экземпляр для вызова f3(i) получится примерно таким:
// недопустимый код, приведен только для примера
void f3<int&>(int& &&); // когда T - это int&, параметр
// функции - это int& &&
Параметр функции f3() — это Т&&, а T — это int&, таким образом, Т&& будет int& &&, что сворачивается в int&. Таким образом, даже при том, что формой параметра функции f3() будет ссылка на r-значение (т.е. T&&), этот вызов создаст экземпляр функции f3() с типом ссылки на l-значение (т.е. int&):
void f3<int&>(int&); // когда T - это int&, параметр функции
// сворачивается в int&
У этих правил есть два важных следствия.
• Параметр функции, являющийся ссылкой на r-значение для параметра типа шаблона (например, Т&&), может быть связан с l-значением.
• Если аргумент будет l-значением, то выведенный тип аргумента шаблона будет типом ссылки на l-значение, и экземпляр параметра функции будет создан как (обычный) параметр ссылки на l-значение (Т&).
Стоит также обратить внимание на то, что параметру функции Т&& косвенно можно передать аргумент любого типа. Параметр такого типа может использоваться с r-значениями, а, как было продемонстрировано только что, также и с l-значениями.
Шаблоны функций с параметрами ссылки на r-значения
У того факта, что параметр шаблона может быть выведен как ссылочный тип, имеются удивительные последствия для кода в шаблоне:
template <typename Т> void f3(Т&& val) {
T t = val; // копировать или привязать ссылку?
t = fcn(t); // изменит ли присвоение только t или val и t?
if (val == t) { /* ... */ } // всегда истинно, если Т - ссылочный тип
}
Когда вызов функции f3() происходит для такого r-значения, как литерал 42, T имеет тип int. В данном случае локальная переменная t имеет тип int и инициализируется при копировании значения параметра val. При присвоении переменной t параметр val остается неизменным.
С другой стороны, когда происходит вызов функции f3() для l-значения i, типом T будет int&. Когда определяется и инициализируется локальная переменная t, у нее будет тип int&. Инициализация переменной t свяжет ее с параметром val. При присвоении переменной t одновременно изменяется и параметр val. В этом экземпляре функции f3() оператор if всегда будет возвращать значение true.
На удивление сложно написать правильный код, когда задействованные типы могут быть простыми (не ссылочными) типами или ссылочными типами (хотя такие классы трансформации типов, как remove_reference (см. раздел 16.2.3), вполне могут помочь в этом).
На практике параметры в виде ссылки на r-значение используются в одном из двух случаев: либо когда шаблон перенаправляет свои аргументы, ли когда шаблон перегружается. Перенаправление рассматривается в разделе 16.2.7, а перегрузка шаблона в разделе 16.3, а пока достаточно знать, что стоит обратить внимание на то, что шаблоны функций, использующие ссылки на r-значение, зачастую используют перегрузку таким же образом, как описано в разделе 13.6.3:
template <typename Т> void f(Т&&); // привязка к не константным
// r-значениям
template <typename Т> void f(const T&); // l-значения и константные
// r-значения
Подобно нешаблонным функциям, первая версия будет связана с изменяемым r-значением, а вторая с l-значением или константным r-значением.
Упражнения раздела 16.2.5
Упражнение 16.42. Определите типы Т и val в каждом из следующих вызовов:
template <typename Т> void g(T&& val);
int i = 0; const int ci = i;
(a) g(i); (b) g(ci); (c) g(i * ci);
Упражнение 16.43. Используя определенную в предыдущем упражнении функцию, укажите, каким будет параметр шаблона g() при вызове g(i = ci)?
Упражнение 16.44. Используя те же три вызова, что и в первом упражнении, определите типы T, если параметр функции g() объявляется как T (а не Т&&) и как const Т&?
Упражнение 16.45. С учетом следующего шаблона объясните происходящее при вызове функции g() с таким литеральным значением, как 42, и с переменной типа int?
template <typename Т> void g(T&& val) { vector<T> v; }