是否有任何用例 std::forward 带有 prvalue

Are there any use cases for std::forward with a prvalue?

本文关键字:forward prvalue 带有 std 任何用 是否      更新时间:2023-10-16

std::forward最常见的用法是完美转发(通用)引用,例如

template<typename T>
void f(T&& param)
{
g(std::forward<T>(param)); // perfect forward to g
}

这里param是一个lvaluestd::forward最终将其转换为右值或左值,这取决于限定在它的参数是什么。

从 cppreference.com 看std::forward的定义,我发现还有一个rvalue重载

template< class T >
T&& forward( typename std::remove_reference<T>::type&& t );

谁能给我任何理由为什么rvalue超载?我看不到任何用例。如果要将 rvalue 传递给函数,可以按原样传递它,无需对其应用std::forward

这与std::move不同,我明白为什么人们还想要一个rvalue重载:你可能会处理你不知道你被传递了什么的泛型代码,你想要对移动语义的无条件支持,例如,参见为什么 std::move 采用通用引用?

编辑为了澄清这个问题,我问为什么从这里重载(2)是必要的,以及它的用例。

好的,因为@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. 使用重载 1,没有它,你会得到编译错误
  2. 使用重载 2,没有它,你会得到编译错误
  3. 使用重载 1,没有它,你会得到编译错误
  4. 使用重载 2,没有它,你会得到编译错误

请注意,如果我们不使用转发

Library a( std::move(v));
//and
Library a( v);

你会得到:

  1. 编译错误
  2. 编译
  3. 编译
  4. 编译

如您所见,如果您只使用两个forward重载中的一个,则基本上会导致不编译 4 个案例中的 2 个,而如果您根本不使用forward,您将只能编译 3 个案例中的 4 个。

这个答案是为了回答@vsoftco的评论

@DarioOO感谢您的链接。你能写一个简洁的答案吗?从您的示例中,我仍然不清楚为什么还需要为 rvalues 定义 std::forward

总之:

因为如果没有右值专用化,以下代码将无法编译

#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 的构造函数,而只有一个右值引用。 如果没有完美的转发,我们最终会陷入不希望的行为:

包装函数在传递重物时会产生高性能损失。

使用移动语义,我们确保尽可能使用移动构造函数。 在上面的例子中,如果我们删除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

因此,需要重载来实现仍然安全的隐式转换,但没有重载就不可能实现。

事实上,下面的代码就是不能编译:

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点

  • 额外的编译时间(实际上没有那么多)
  • 可能会导致不需要的副本(当您没有明确地将某些内容移动到需要右值的内容中时)

如果使用得小心,是一个强大的工具。

我之前盯着这个问题,看了霍华德·欣南特的链接,思考了一个小时后无法完全理解。现在我正在寻找并在五分钟内得到了答案。(编辑:得到答案太慷慨了,因为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;
}

此代码打印"移动"。有趣的是,如果我删除std::forward,它会打印副本。对我来说,这很难让我思考,但让我们接受它并继续前进。(编辑:我想发生这种情况是因为 get 将返回对右值的左值引用。这样的实体衰减成左值,但 std::forward 会将其转换为右值,就像 forward 的常见用法一样。不过还是觉得不直观)。

现在,让我们想象另一个类:

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时,我们希望它与HelloHello2正常工作,即我们想要触发一个移动。如果我们包括外std::forward,这只会发生在Hello的右值,所以我们需要它。但。。。我们到了重点。当我们将Hello2的右值传递给此函数时,get() 的右值重载已经返回右值双精度,因此std::forward实际上接受右值。因此,如果没有,您将无法编写如上所述的完全通用代码。

该死的。