Существуют ли какие-либо варианты использования std:: forward с prvalue?
Наиболее распространенное использование std::forward
заключается в том, чтобы идеально переадресовать перенаправление (универсальную) ссылку, например
template<typename T>
void f(T&& param)
{
g(std::forward<T>(param)); // perfect forward to g
}
Здесь param
- это lvalue
, а std::forward
заканчивается тем, что он присваивает значение rvalue или lvalue, в зависимости от того, что ограничивал его аргумент.
Глядя на определение std::forward
на cppreference.com Я вижу, что существует также rvalue
перегрузка
template< class T >
T&& forward( typename std::remove_reference<T>::type&& t );
Может ли кто-нибудь объяснить мне причину перегрузки rvalue
? Я не вижу никакого варианта использования. Если вы хотите передать значение r функции, вы можете просто передать его как есть, не нужно применять std::forward
на нем.
Это отличается от std::move
, где я вижу, почему нужно также перегружать rvalue
: вы можете иметь дело с общим кодом, в котором вы не знаете, что вам передается, и вы хотите безоговорочную поддержку для перемещения семантики, см., например, Почему std:: move принимает универсальную ссылку?.
EDIT Чтобы прояснить вопрос, я спрашиваю, почему перегрузка (2) здесь необходима, и прецедент для него.
Ответы
Ответ 1
Хорошо, так как @vsoftco попросила краткую версию использования здесь уточненную версию (используя его идею иметь "my_forward", чтобы на самом деле увидеть, как вызвана перегрузка).
Я интерпретирую "пример использования", предоставляя образец кода, который без prvalue не компилируется или ведет себя по-другому (независимо от того, что было бы действительно полезно или нет).
У нас 2 перегрузки для std::forward
#include <iostream>
template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type& t) noexcept
{
std::cout<<"overload 1"<<std::endl;
return static_cast<T&&>(t);
}
template <class T>
inline T&& my_forward(typename std::remove_reference<T>::type&& t) noexcept
{
std::cout<<"overload 2"<<std::endl;
static_assert(!std::is_lvalue_reference<T>::value,
"Can not forward an rvalue as an lvalue.");
return static_cast<T&&>(t);
}
И у нас есть 4 возможных варианта использования
Использовать случай 1
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &&
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(v)); // &
return 0;
}
Использовать случай 2
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &&
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(std::move(v))); //&&
return 0;
}
Пример использования 3
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &
Library( vector<int> a):b(a){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(v)); // &
return 0;
}
Случай использования 4
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// &
Library( vector<int> a):b(a){
}
};
int main()
{
vector<int> v;
v.push_back(1);
Library a( my_forward<vector<int>>(std::move(v))); //&&
return 0;
}
Здесь резюме
- Используется перегрузка 1, без нее вы получаете ошибку компиляции
- Используется перегрузка 2, без нее вы получаете ошибку компиляции
- Используется перегрузка 1, с которой вы получаете ошибку компиляции.
- Используется перегрузка 2, без нее вы получаете ошибку компиляции
Обратите внимание, что если мы не используем forward
Library a( std::move(v));
//and
Library a( v);
вы получаете:
- Ошибка компиляции
- Compile
- Compile
- Compile
Как вы видите, если вы используете только одну из двух перегрузок forward
, вы в основном вынуждаете не компилировать 2 из 4 случаев, а если вы не используете forward
вообще, вы можете скомпилировать только 3 из 4 случаев.
Ответ 2
Этот ответ предназначен для ответа на комментарий @vsoftco
@DarioOO благодарит за ссылку. Можете ли вы написать краткий ответ? Из вашего примера мне все еще не ясно, почему std:: forward необходимо также определить для rvalues
Короче:
Поскольку без специализации rvalue следующий код не будет компилировать
#include <utility>
#include <vector>
using namespace std;
class Library
{
vector<int> b;
public:
// hi! only rvalue here :)
Library( vector<int>&& a):b(std::move(a)){
}
};
int main()
{
vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v));
return 0;
}
однако я не могу удержаться, чтобы напечатать более так же здесь и не сукцинтную версию ответа.
Длинная версия:
Вам нужно переместить v
, потому что класс Library
не имеет конструктора, принимающего значение lvalue, а только ссылки rvalue.
Без совершенной пересылки мы оказались бы в нежелательном поведении:
функции обертывания будут приводить к высокой эффективности при передаче тяжелых объектов.
с семантикой перемещения, мы убеждаемся, что конструктор перемещения используется IF ВОЗМОЖНО.
В приведенном выше примере, если мы удалим std::forward
, код не будет компилироваться.
Итак, что на самом деле делает forward
? перемещение элемента без нашего консенсуса? Неа!
Он просто создает копию вектора и перемещает его. Как мы можем быть уверены в этом? Просто попробуйте обратиться к элементу.
vector<int> v;
v.push_back(1);
A a( forward<vector<int>>(v)); //what happens here? make a copy and move
std::cout<<v[0]; // OK! std::forward just "adapted" our vector
если вы переместите этот элемент
vector<int> v;
v.push_back(1);
A a( std::move(v)); //what happens here? just moved
std::cout<<v[0]; // OUCH! out of bounds exception
Чтобы перегрузка была необходима для обеспечения неявного преобразования, которое по-прежнему безопасно, но не возможно без перегрузки.
Infact, следующий код просто не компилируется:
vector<int> v;
v.push_back(1);
A a( v); //try to copy, but not find a lvalue constructor
Реальный прецедент:
Вы можете утверждать, что аргументы пересылки могут создавать бесполезные копии и, следовательно, скрыть возможное поражение производительности, да, это действительно так, но рассмотрите реальные варианты использования:
template< typename Impl, typename... SmartPointers>
static std::shared_ptr<void>
instancesFactoryFunction( priv::Context * ctx){
return std::static_pointer_cast<void>( std::make_shared<Impl>(
std::forward< typename SmartPointers::pointerType>(
SmartPointers::resolve(ctx))...
) );
}
Код был взят из моей рамки (строка 80): Infectorpp 2
В этом случае аргументы пересылаются из вызова функции. SmartPointers::resolve
возвращаемые значения корректно перемещаются независимо от того, что конструктор Impl
принимает значение rvalue или lvalue (так что ошибки компиляции и все равно не перемещаются).
В принципе вы можете использовать std::foward
в любом случае, в котором вы хотите сделать код более простым и понятным, но вы должны иметь в виду 2 балла
- дополнительное время компиляции (не так много на самом деле)
- может вызывать нежелательные копии (когда вы явно не перемещаете что-то во что-то, что требует rvalue)
Если использовать с осторожностью, это мощный инструмент.
Ответ 3
Я смотрел на этот вопрос раньше, прочитал ссылку Ховарда Хиннанта, не мог полностью заглянуть в него после часа размышлений. Теперь я смотрел и получил ответ через пять минут. (Edit: получил ответ слишком щедрым, так как ссылка Hinnant имела ответ. Я имел в виду, что я понял и смог объяснить это более простым способом, который, надеюсь, кто-то найдет полезным).
В принципе, это позволяет вам быть общим в определенных ситуациях, в зависимости от введенного кода. Рассмотрим этот код:
#include <utility>
#include <vector>
#include <iostream>
using namespace std;
class GoodBye
{
double b;
public:
GoodBye( double&& a):b(std::move(a)){ std::cerr << "move"; }
GoodBye( const double& a):b(a){ std::cerr << "copy"; }
};
struct Hello {
double m_x;
double & get() { return m_x; }
};
int main()
{
Hello h;
GoodBye a(std::forward<double>(std::move(h).get()));
return 0;
}
Этот код печатает "move". Интересно, что если я удаляю std::forward
, он печатает копию. Это, для меня, трудно оборачивать мой разум, но позвольте принять его и двигаться дальше. (Edit: Я предполагаю, что это происходит потому, что get вернет ссылку lvalue на rvalue. Такая сущность распадается на lvalue, но std:: forward будет использовать ее в rvalue, как и при обычном использовании переадресации. хотя).
Теперь представьте себе другой класс:
struct Hello2 {
double m_x;
double & get() & { return m_x; }
double && get() && { return std::move(m_x); }
};
Предположим, что в коде в main
h
был экземпляр Hello2. Теперь нам больше не нужен std:: forward, потому что вызов std::move(h).get()
возвращает rvalue. Однако предположим, что код является общим:
template <class T>
void func(T && h) {
GoodBye a(std::forward<double>(std::forward<T>(h).get()));
}
Теперь, когда мы вызываем func
, мы хотели бы, чтобы он корректно работал как с Hello
, так и с Hello2
, т.е. мы хотели бы вызвать движение. Это происходит только для rvalue Hello
, если мы включаем внешний std::forward
, поэтому нам это нужно. Но... Мы добрались до пергамента. Когда мы передаем rvalue Hello2
этой функции, перегрузка rvalue из get() уже вернет значение rvalue double, поэтому std::forward
фактически принимает значение r. Поэтому, если это не так, вы не сможете писать полностью общий код, как указано выше.
Блин.