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-значение, которое было дано.
Теперь рассмотрим второе присвоение, которое вызывает функцию std::move(s1). В этом вызове аргументом функции move() является l-значение. Поэтому на сей раз:
• выведенным типом Т будет string& (ссылка на тип string, а не просто string);
• следовательно, экземпляр шаблона remove_reference создается с типом string&;
• тип-член type класса remove_reference<string&> будет иметь тип string;
• типом возвращаемого значения функции move() все еще будет string&&;
• параметр t функции move() будет создан как экземпляр string& &&, который сворачивается в string&.
Таким образом, этот вызов создает экземпляр шаблона move<string&>, который является точно тем, что необходимо для связи ссылки на r-значение с l-значением.
string&& move(string &t)
Тело этого экземпляра возвращает тип static_cast<string&&>(t). В данном случае типом t является string&, который приведение преобразует в тип string&&.
Оператор static_cast поддерживает приведение l-значения к ссылке на r-значение
Привязка ссылки на r-значение к l-значению создает код, который работает с разрешением ссылке на r-значение заменять l-значение. Иногда, как в случае с функцией reallocate() класса StrVec (см. раздел 13.6.1), известно, что замена l-значения безопасна. Разрешая осуществлять это приведение, язык позволяет его использование. Вынуждая использовать приведение, язык пытается предотвратить его случайное использование.
И наконец, хотя такие приведения можно написать непосредственно, намного проще использовать библиотечную функцию move(). Кроме того, использование функции std::move() существенно облегчает поиск в коде места, потенциально способного заменить l-значения.
Упражнения раздела 16.2.6
Упражнение 16.46. Объясните, что делает этот цикл из функции StrVec::reallocate() (раздел 13.5):
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));