Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
Комбинация правил свертывания ссылок и специального правила дедукции типа для ссылочных на 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-значение на тип параметра шаблона (т.е. Т&&), может быть передан аргумент любого типа. Когда такому параметру передается l-значение, экземпляр параметра функции создается как обычная ссылка на l-значение (T&).
Шаблоны функций с параметрами ссылки на 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; }
16.2.6. Функция std::move()
Библиотечная функция move() (см. раздел 13.6.1) — хороший пример шаблона, использующего ссылки на r-значение. К счастью, функцию move() можно использовать, не понимая механизма работы используемого ею шаблона. Однако изучение работы функции move() может помочь понять и использовать шаблоны.
В разделе 13.6.2 обращалось внимание на то, что, хотя и нельзя непосредственно привязать ссылку на r-значение к l-значению, функцию move() можно использовать для получения ссылки на r-значение, связанной с l-значением. Поскольку функция move() может получать аргументы, по существу, любого типа, нет ничего удивительного в том, что move() — это шаблон функции.
Как определена функция std::move()Стандартное определение функции move() таково:
// об использовании typename в типе возвращаемого значения и
// приведении см. раздел 16.1.3
// remove_reference рассматривается в разделе 16.2.3
template <typename Т>
typename remove_reference<T>::type&& move(T&& t) {
// static_cast рассматривается в разделе 4.11.3
return static_cast<typename remove_reference<T>::type&&>(t);
}
Этот код короток, но сложен. В первую очередь, параметр функции move(), Т&& является ссылкой на r-значение типа параметра шаблона. Благодаря сворачиванию ссылок этот параметр может соответствовать аргументу любого типа. В частности, функции move() можно передать либо l-, либо r-значение:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: перемещение r-значения
s2 = std::move(s1); // ok: но после присвоения
// значение s1 неопределенно
Как работает функция std::move()В первом присвоении аргумент функции move() является r-значением, полученным в результате выполнения конструктора string("bye") класса string. Как уже упоминалось, при передаче r-значения ссылочному r-значению параметра функции выведенный из этого аргумента тип является ссылочным типом (см. раздел 16.2.5). Таким образом, в вызове std::move(string("bye!")):
• выведенным типом T будет string;
• следовательно, экземпляр шаблона remove_reference создается с типом string;
• тип-член type класса remove_reference<string> будет иметь тип string;
• типом возвращаемого значения функции move() будет string&&;
• у параметра t функции move() будет тип string&&;
Соответственно, этот вызов создает экземпляр move<string>, являющийся следующей функцией:
string&& move(string &&t)
Тело этой функции возвращает тип static_cast<string&&>(t). Типом t уже является string&&, поэтому приведение не делает ничего. Следовательно, результатом этого вызова будет ссылка на r-значение, которое было дано.