Что конкретно делают std::move и std::forward?
Я знаком с Move Semantics и Perfect Forwarding. Но меня мучает один вопрос, который заключается в следующем:
void function(Type&& argument)
{
std::is_lvalue_reference<decltype(argument)>::value;
std::is_rvalue_reference<decltype(argument)>::value;
std::move(argument);
}
template <typename Type>
void function(Type&& argument)
{
std::is_lvalue_reference<decltype(argument)>::value;
std::is_rvalue_reference<decltype(argument)>::value;
std::forward<Type>(argument);
}
Если воспользоваться std::is_lvalue_reference и std::is_rvalue_reference, то станет понятно, что С++ знает, о том, что внутри функций (lvalue или rvalue). А значит std::forward делает следующее: если приходит rvalue - он возвращает rvalue, если приходит lvalue - он возвращает lvalue. Т.е. происходит какая-то, на первый взгляд, бессмысленная работа. Однако без std::forward "идеальная передача" не работает. А без std::move не работает "семантика перемещения". Проверено экспериментально. Так в чём же всё-таки дело?!
Ответы (1 шт):
Рассмотрим простой пример
template<typename T>
T&& func(T&& t)
{
return t;
}
int main()
{
double v1 = 0.9;
const double v2 = 0.9;
func(v1);//Ok
func(v2);//Ok
func(42);//Ошибка
}
Для того, чтобы разобраться что же здесь происходит разберем два правила, работающих в контексте вывода типа.
Правило первое. При выводе типа (в шаблоне или для auto) ссылочность типа отбрасывается, т.е., если тип будет выводится из аргумента, то тип будет не ссылочным, за исключением случаев, когда тип выводится совместно с использованием rvalue-ссылки, но об этом чуть позже. Если нужна ссылочность, то при таком шаблоне нужно указывать тип при использовании:
template<class T>
void func(T val)
{}
//...
double x{};
func(x);//T = double
func(std::move(x));//T = double
func(33.6);//T = double
func<double&>(x);//T = double&
func<double&&>(std::move(x));//T = double&&
func<double&&>(33.8);//T = double&&
Правило второе. Ссылки умеют сворачиваться (схлопываться) при выводе типа. Это значит, что если при выводе типа получается "ссылка на ссылку", то она сворачивается. Правила сворачивания приведу как табличку истинности:
& &&
& & &
&& & &&
т.е. если у нас получается "ссылка на ссылку", то конечным типом всегда будет lvalue-ссылка, за исключением случаем, когда получается rvalue-ссылка на rvalue-ссылку, в таком случае конечным типом будет rvalue-ссылка.
Чтобы стало понятнее, посмотрим на примеры с typedef'ами:
typedef double& LVR; //double&
typedef LVR& LLVR; //double& + & = double&
typedef LVR&& RLVR; //double& + && = double&
typedef double&& RVR; //double&&
typedef RVR& LRVR; //double&& + & = double&
typedef RVR&& RRVR; //double&& + && = double&&
Эти два правила при выводе типа позволяют организовать аргументы, которые могут ссылаться на всё что угодно.
Взглянем на первый пример еще раз:
template<class T> T&& func(T&& t)
{
return t;
};
int main() {
double v1 = 0.9;
const double v2 = 0.9;
func(v1);//1
func(v2);//2
func(42);//3
};
Ситуация первая — func(v1):
v1 имеет тип double.
в параметрах func принимает аргумент типа T&& (при этом T выводится из аргумента). Выражение v1 будет иметь категорию lvalue, значит v1 не может быть привязан к double&&. Однако, если вывести T не как double, а как double&, то мы получаем ситуацию double& &&, а значит ссылка будет свёрнута до double&. Получается, что тип T нужно вывести именно как lvalue-reference - double& для успеха. В возвращаемом значении у нас T&&, а значит при выведенном T как double& мы получаем double& &&, которое также схлопнется до double&. Получаем функцию:
double& func(double& r)
{
return r;
}
очевидно, что в данной функции всё хорошо и никаких ошибок.
Во втором случае — fucn(v2),
v2 имеет тип const double, так что всё будет также, как и с v1, за исключением того, что типы будут иметь квалификатор const:
const double& fun(const double& r)
{
return r;
}
А третий случай в нашем коде интереснее — func(67).
67 — даст временный объект типа int, т.е. у нас получается выражение с категорией rvalue. rvalue может быть привязано к не константной rvalue-ссылке, а значит T будет выведен как double. Для успеха вывода типа T можно взять как double, и тогда получается код:
double&& fun(double&& r)
{
return r;//lvalue с типом double&& - ошибка
}
Так почему же происходит ошибка? А потому, что сама переменная r — это rvalue-reference, но она — именованная переменная, и выражение из её имени (r) будет иметь категорию lvalue. Как известно, lvalue неявно к rvalue-reference не преобразуется, поэтому и возникает ошибка при попытке возврата ссылки r. А теперь добавим std::forward в код:
template<class T> T&& func(T&& t)
{
return std::forward<T>(t);//Добавили std::forward - всё хорошо.
};
Почему исчезла ошибка? Рассмотрим упрощенное устройство std::forward и напишем свой простенький аналог.
//std::forward
template <class T> constexpr T&& forward(typename remove_reference<T>::type& t) noexcept;
template <class T> constexpr T&& forward(typename remove_reference<T>::type&& t) noexcept;
//Returns: static_cast<T&&>(t).
Реализуем свой forward, но упрощения ради отбросим constexpr и пр.:
template <class T>
T&& forward(std::remove_reference_t<T>& t) noexcept
{
return static_cast<T&&>(t);
}
template <class T>
T&& forward(std::remove_reference_t<T>&& t) noexcept
{
return static_cast<T&&>(t);
}
сопоставим это с нашей функцией:
template<class T> T&& func(T&& t)
{
return forward<T>(t);//Тоже всё работает и ошибок нет
}
Так в чем же магия?
Предположим, что мы вызвали func с нашим v1 — lvalue, тогда получаем, что T вывелся как double&, а значит t — это lvalue-reference.
Теперь рассмотрим работу forward<T&>. Мы инстанцируем его с типом T — double&.
Значит получаем такой промежуточный код:
double& && forward(std::remove_reference_t<double&>& t) noexcept
{
return static_cast<double& &&>(t);
}
remove_reference_t уберет ссылку, а в static_cast и в возвращаемом значении ссылки свернуться и получится функция:
double& forward(double& t) noexcept
{
return static_cast<double&>(t);
}
в итоге, forward даст double&. Следовательно, передав в func lvalue, мы получаем:
double& func(double& t)
{
return forward<double&>(t);//lvalue с типом double&
}
А вызовем func с аргументом (выражением), имеющим категорию rvalue (xvalue) — например, std::move(v1).
Получаем, что тип T в func выведется как double, а значит мы инстанцируем forward с типом double (forward<double>(t)), при этом передаем в качестве аргумента функции rvalue-reference, и получим такой промежуточный код:
double&& forward(std::remove_reference_t<double>& t) noexcept
{
return static_cast<double&&>(t);
}
И в итоге вся функция func будет безошибочна:
double&& func(double&& t)
{
return forward<double>(t); //xvalue с типом double&&
}
В этом коде forward уже вернет double&& (категория выражения - xvalue), а не double& (категория выражения - lvalue).
То есть, forward, в зависимости от аргумента шаблона и аргумента шаблонной функции, на выходе даст либо rvalue-reference и всё выражение будет иметь категорию xvalue, либо lvalue-reference и всё выражение будет иметь категорию lvalue. На этом вся магия кончается. Пара простых правил и зелье готово.
Что касается std::move - то это просто static_cast до rvalue-ссылки. Он всегда возвращает rvalue-reference и всё выражение имеет категорию xvalue.