Несмотря на то, что материалов на тему 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) в терминологии комитета стандартизации С++, то есть она:

  1. Выводимая

  2. Имеет вид 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»