Несмотря на то, что материалов на тему move-семантики и идеальной передачи в Интернете предостаточно, вопросов типа «что я должен здесь использовать: move или forward?» не становится меньше или мне просто «везет» на них. Поэтому и решено было написать эту статью. Предполагается, что читатель хотя бы немного знаком с rvalue-ссылками, move-семантикой и идеальной передачей.

Для чего нужен шаблон функции std::move?
Функция std::move выполняет приведение передаваемого lvalue-аргумента в rvalue-ссылку. Зачем это нужно? Вернемся во времена С++98 и рассмотрим классический пример:
template<class T> swap(T& a, T& b) { T tmp(a); // здесь две копии a a = b; // здесь две копии b b = tmp; // здесь две копии a }
В этом примере для встроенных типов проблем не возникает, но для таких типов как vector или string копирование может являться ��райне дорогой операцией. Необходимо было добавлять специализации для шаблона swap, чтобы выполнять это действие более эффективно, и многих такой подход не устраивал.
Что же здесь нужно изменить? Нам не нужно выполнять копирование, а нужно «перемещать» объекты. Как это сделать? Нужно вызывать специальный конструктор или оператор присваивания, которые не будут копировать содержимое классов, а будут обмениваться им (по сути выполнять swap для всех членов).
Для этого в C++11 ввели rvalue-ссылки, которые обозначаются через && и позволяют ссылаться на временные объекты или определять объекты как «перемещаемые». Также появились конструктор перемещения (move constructor) и оператор перемещающего присваивания (move assignment), которые отличаются от копирующих «коллег» тем, что в качестве аргумента принимают неконстантную rvalue-ссылку:
template<class T> class vector { // ... vector(const vector&); // copy constructor vector(vector&&) noexcept; // move constructor vector& operator=(const vector&); // copy assignment vector& operator=(vector&&); // move assignment };
Теперь операцию swap мы можем переписать с использованием функции std::move, которая возвращает rvalue-ссылку и тем самым сообщает компилятору, что параметр является перемещаемым:
template<class T> void swap(T& a, T& b) { T tmp = std::move(a); a = std::move(b); b = std::move(tmp); }
Функция std::move не выполняет никаких перемещений. Как уже было сказано выше, она выполняет приведение типа к rvalue-ссылке. Давайте посмотрим на ее код:
// FUNCTION TEMPLATE move template <class _Ty> [[nodiscard]] constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable return static_cast<remove_reference_t<_Ty>&&>(_Arg); }
То есть это просто обертка для static_cast, которая «убирает» ссылку у переданного аргумента с помощью remove_reference_t и, добавив &&, преобразует тип в rvalue-ссылку. Давайте глянем на то, как именно remove_reference_t избавляется от ссылок:
// STRUCT TEMPLATE remove_reference template <class _Ty> struct remove_reference { using type = _Ty; using _Const_thru_ref_type = const _Ty; }; template <class _Ty> struct remove_reference<_Ty&> { using type = _Ty; using _Const_thru_ref_type = const _Ty&; }; template <class _Ty> struct remove_reference<_Ty&&> { using type = _Ty; using _Const_thru_ref_type = const _Ty&&; }; template <class _Ty> using remove_reference_t = typename remove_reference<_Ty>::type;
Для чего нужен std::forward?
Функция std::forward, как известно, применяется при идеальной передаче (perfect forwarding).
Идеальная передача позволяет создавать функции-обертки, передающие параметры без каких-либо изменений (lvalue передаются как lvalue, а rvalue – как rvalue) и тут std::move нам не подходит, так как она безусловно приводит свой результат к rvalue.
Поэтому, была разработана функция std::forward, которая выполняет примерно следующую работу:
template<typename T> T&& forward(T&& param) { if (is_lvalue_reference<T>::value) return param; else return move(param); }
То есть, если ссылка была передана как rvalue, то вызываем духов std::move, а иначе просто возвращаем то, что передали.
Если посмотреть исходники стандартной библиотеки у MS, то мы увидим следующую реализацию:
// FUNCTION TEMPLATE forward template <class _Ty> [[nodiscard]] constexpr _Ty&& forward( remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue return static_cast<_Ty&&>(_Arg); } template <class _Ty> [[nodiscard]] constexpr _Ty&& forward( remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"); return static_cast<_Ty&&>(_Arg); }
Может быть немного сложнее, но смысл тот же. И пусть Вас не пугает перегруженная версия forward(remove_reference_t<_Ty>&& _Arg). В ней просто добавлена проверка на случай компиляции чудесатых конструкций вроде такой:
func(std::forward<int&>(7));
На стадии компиляции получим сразу ошибку: bad forward call. И я полагаю, это единственное, ради чего добавлена еще одна сигнатура, поскольку нет никакого смысла городить, например, такие конструкции:
bar(std::forward<int>(1));
Если у кого-то есть идеи, для чего еще может понадобиться forward(remove_reference_t<_Ty>&& _Arg), дайте знать и заранее спасибо.
Возможно сейчас у читателя возникает в голове вопрос: а может на самом деле нет нужды вызывать forward и просто вызывать static_cast<_Ty&&>(_Arg)? Да можно, конечно, просто forward более безопасен в плане очепяток и путаниц. Кроме того, forward<T>(Arg) в коде выглядит более осмысленным и понятным, нежели static_cast<T&&>(Arg). Никто же не любит писать комментарии, поэтому лучше писать сразу хорошо читаемый код. То же самое касается и move, и тут придется писать более громоздкую конструкцию:
static_cast<remove_reference_t<T>&&>(Arg);
Пример
#include <iostream> using namespace std; template<typename T> void bar(const T& v) { cout << "by const ref" << endl; } template<typename T> void bar(T& v) { cout << "by lvalue ref" << endl; } template<typename T> void bar(T&& v) { cout << "by rvalue ref" << endl; } // FUNCTION TEMPLATE forward template <class _Ty> [[nodiscard]] constexpr _Ty&& _forward( remove_reference_t<_Ty>& _Arg) noexcept { cout << "forward an lvalue as either an lvalue or an rvalue" << endl; return static_cast<_Ty&&>(_Arg); } template <class _Ty> [[nodiscard]] constexpr _Ty&& _forward( remove_reference_t<_Ty>&& _Arg) noexcept { static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call"); cout << "forward an rvalue as an rvalue" << endl; return static_cast<_Ty&&>(_Arg); } // FUNCTION TEMPLATE move template <class _Ty> [[nodiscard]] constexpr remove_reference_t<_Ty>&& _move(_Ty&& _Arg) noexcept { cout << "forward _Arg as movable" << endl; return static_cast<remove_reference_t<_Ty>&&>(_Arg); } template<typename T> void foo(T&& p) { bar(p); bar(_move(p)); bar(_forward<T>(p)); } int main() { int i = 0; foo(i); // lvalue: T - int&, p - int& foo(0); // rvalue: T - int, p - int&& }
Я скопировал код move и forward, вставив в них вывод в cout, чтобы можно было увидеть, что же происходит. Далее мы берем и последовательно вызываем функцию foo, передавая сперва lvalue значение, а затем rvalue. Обратите внимание, что у функции foo аргумент является «универсальной ссылкой» в терминологии Скотта Мейерса, или передаваемой ссылкой (forwarding reference) в терминологии комитета стандартизации С++, то есть она:
Выводимая
Имеет вид T&&
Внутри функции foo, вызываем перегруженную функцию bar для константной lvalue-ссылки, для lvalue-ссылки и, наконец, для «универсальной» ссылки.
На выводе получаем:
by lvalue ref forward _Arg as movable by rvalue ref forward an lvalue as either an lvalue or an rvalue by lvalue ref by lvalue ref forward _Arg as movable by rvalue ref forward an lvalue as either an lvalue or an rvalue by rvalue ref
Итак, разбираем lvalue. В функции foo тип T будет выведен как int&. Почему T выводится как lvalue-сылка, т.е. как int&? По правилам вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано lvalue значение, то T выводи��ся как lvalue-ссылка. Согласно правилам сжатия ссылок (reference collapsing) аргумент p также будет выведен как int&, так как foo(int& && p) превратится в foo(int& p). Таким образом, мы получаем следующую версию foo:
void foo(int& p) { bar(p); bar(_move(p)); bar(_forward<int&>(p)); }
bar(p); вызовет bar(int& v), и тут вопросов возникать не должно.
bar(_move(p)); сперва вызовет move, которая вернет rvalue-ссылку, а значит будет вызвана bar(int&& v).
bar(_forward<int&>(p)); сперва вызовет _forward(int& _Arg), которая должна вернуть lvalue-ссылку, и значит будет вызвана опять bar(int& v). Что при этом произойдет внутри _forward? Применив правила сжатия ссылок, мы получим следующую версию _forward:
constexpr int& _forward(int& _Arg) noexcept { return static_cast<int&>(_Arg); }
Разберем теперь rvalue. В функции foo тип T будет выведен как int. Согласно все тех же правил вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано rvalue, то T выводится как бессылочный тип. Это необходимо для корректной работы функции forward, как мы это увидим далее. Итак, наша foo выглядит теперь следующим образом:
void foo(int&& p) { bar(p); bar(_move(p)); bar(_forward<int>(p)); }
bar(p); как и ранее, вызовет bar(int& v), т.к. p является l-value, не смотря на то, что выглядит как r-value ссылка.
bar(_move(p)); как и ранее, сперва вызовет move, которая вернет rvalue-ссылку, а значит будет опять вызвана bar(int&& v).
Теперь посмотрим как же будет инстанцирована forward<int>:
constexpr int&& _forward(int& _Arg) noexcept { return static_cast<int&&>(_Arg); }
Давайте подставим вместо move и forward их содержимое, чтобы наглядно посмотреть на разницу:
// lvalue void foo(int& p) { bar(p); bar(static_cast<int&&>(p)); bar(static_cast<int&>(p)); } // rvalue void foo(int p) { bar(p); bar(static_cast<int&&>(p)); bar(static_cast<int&&>(p)); }
Вывод: для перемещаемых объектов необходимо использовать std::move, а для идеальной передачи – std::forward.
Примеры использования move и forward
Кроме приведенного выше примера std::swap, использование std:move можно найти в различных алгоритмах, где нужно менять элементы местами (различные сортировки, или, например, в функции std::unique).
Если необходимо «передать» умный указатель std::unique_ptr, то сделать мы можем это только через std::move (либо через release() и сырой указатель, но это не по фень-шую).
В функции std::vector::push_back для rvalue можно обнаружить:
void push_back(_Ty&& _Val) { // insert by moving into element at end, provide strong guarantee emplace_back(std::move(_Val)); }
Таким образом, legacy-код, добавляющий новый элемент в вектор через rvalue волшебным образом начинает работать через перемещение, а не копирование.
Если ваша функция возвращает кортеж (или пару), то стоит обратить внимание на возможность перемещения некоторых или даже всех его элементов:
std::pair<some_type, bool> get_my_data(const size_t index) { some_type my_data; bool is_valid; // do something return { std::move(my_data), is_valid }; }
Обратите внимание на то, что не нужно использовать std::move при возврате из функции, возвращающий локальный объект:
std::string get_my_string(const size_t index) { std::string my_string; // do something return std::move(my_string); // wrong! }
Здесь нужно убрать std::move. Всю работу сделает copy/move elision – специальная оптимизация, которую выполняет компилятор, убирая лишние создания объектов.
Функция std::forward и вариативные шаблоны являются фундаментом, на котором строятся такие функции-обертки как std::make_unique, std::make_shared, std::make_pair, std::make_tuple и другие. Например, make_unique делает очень простую работу:
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0> [[nodiscard]] unique_ptr<_Ty> make_unique(_Types&&... _Args) { // make a unique_ptr return unique_ptr<_Ty>(new _Ty(std::forward<_Types>(_Args)...)); }
Семейство emplace методов также работает через forward, зачастую просто вызывая конструктор через placement-new.
Что еще можно почитать на эту тему:
Идеальная передача и универсальные ссылки в C++
Книга Скотта Мейерса «Эффективный и современный С++. 42 рекомендации по использованию C++11 и C++14»
