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-значение

Обычно оператор static_cast может выполнить только доступные преобразования (см. раздел 16.3). Однако для ссылок на r-значение есть специальное разрешение: даже при том, что нельзя неявно преобразовать l-значение в ссылку на r-значение, используя оператор 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++));