在完美转发中,使用 std::forward
将命名的右值引用 t1
和 t2
转换为未命名的右值引用。这样做的目的是什么?如果我们将 t1
和 t2
保留为左值,那么这会对被调用的函数 inner
产生什么影响呢?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
在完美转发中,使用 std::forward
将命名的右值引用 t1
和 t2
转换为未命名的右值引用。这样做的目的是什么?如果我们将 t1
和 t2
保留为左值,那么这会对被调用的函数 inner
产生什么影响呢?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
E(a, b, ... , c)
,我们希望表达式f(a, b, ... , c)
是等价的。在C++03中,这是不可能的。有许多尝试,但它们都在某些方面失败。
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
f(1, 2, 3);
,因为它们无法绑定到左值引用。template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
以上问题可以通过"const X&
绑定到任何东西"来解决,包括左值和右值,但这会引起一个新的问题。现在无法允许E
具有非const
参数:
int i = 1, j = 2, k = 3;
void E(int&, int&, int&);
f(i, j, k); // oops! E cannot modify these
第三种尝试接受const引用,但是然后使用const_cast
去除了const
:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
这个接受所有值,可以传递所有值,但可能导致未定义的行为:
const int i = 1, j = 2, k = 3;
E(int&, int&, int&);
f(i, j, k); // ouch! E can modify a const object!
f
的重载,涵盖所有const和non-const的组合:template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
N个参数需要2N种组合,这是一场噩梦。我们希望能够自动完成这个任务。(在C++11中,我们可以让编译器为我们完成这项工作。)
在C++11中,我们有机会解决这个问题。其中一种解决方案修改了现有类型的模板推导规则,但这可能破坏大量代码。 因此,我们必须找到另一种方法。
解决方案是使用新添加的rvalue引用;我们可以在推断rvalue引用类型时引入新规则,并创建任何所需的结果。毕竟,我们现在不可能破坏代码了。
如果给定对引用的引用(注意引用是一个包含T&和T&&的总称),我们使用以下规则来确定结果类型:
"[给定]一个类型TR,它是类型T的引用,试图创建类型“cv TR的左值引用”将创建类型“T的左值引用”,而试图创建类型“cv TR的右值引用”将创建类型TR。"
或者以表格形式呈现:
TR R
T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
void foo(int&);
template <typename T>
void deduce(T&& x)
{
foo(x); // fine, foo can refer to x
}
deduce(1); // okay, foo operates on x which has a value of 1
这不太好。E需要获得与我们相同类型的值类别!解决方案如下:
static_cast<T&&>(x);
deduce
函数内部,并且我们已经传递了一个左值。这意味着T
是A&
,因此静态转换的目标类型是A& &&
或者只是A&
。由于x
已经是A&
,所以我们不需要进行任何操作,只剩下一个左值引用。T
是A
,因此静态转换的目标类型是A&&
。转换结果是一个右值表达式,不能再传递给左值引用。我们保持了参数的值类别。template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
f
接收到左值时,E
会得到一个左值。当f
接收到右值时,E
会得到一个右值。完美。
static_cast<T&&>
很晦涩难懂,不容易记住;我们可以改用一个实用函数 forward
来完成同样的操作:std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
forward
和move
是否有区别?还是只是语义上的差别? - David Gstd::move
应该不带显式模板参数调用,并且始终产生一个右值,而std::forward
可能会成为任何类型。当你知道你不再需要一个值并想将其移动到其他地方时,请使用std::move
,根据传递给函数模板的值来使用std::forward
。 - GManNickGstatic_cast<A&&>(a)
被视为第0种情况,但在6个测试中失败了两个,详情请参见 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html。 - user2023370我认为使用一个概念性代码实现std::forward有助于理解。这是Scott Meyers在他的演讲“An Effective C++11/14 Sampler”中的一张幻灯片(链接)
代码中的move
函数是std::move
。在那个演讲之前,有一个有效的实现。我在文件move.h中找到了libstdc++中std::forward的实际实现,但它并不具有指导意义。
从用户的角度来看,它的含义是std::forward
是对rvalue的条件转换。如果我写一个函数,该函数希望在参数中既接受lvalue又接受rvalue,并且只想在传递它作为rvalue时将其作为rvalue传递给另一个函数,则可以使用std::forward进行包装。如果我没有使用std::forward包装参数,则永远会将其作为普通引用传递。
#include <iostream>
#include <string>
#include <utility>
void overloaded_function(std::string& param) {
std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
std::cout << "std::string&& version" << std::endl;
}
template<typename T>
void pass_through(T&& param) {
overloaded_function(std::forward<T>(param));
}
int main() {
std::string pes;
pass_through(pes);
pass_through(std::move(pes));
}
果然,它会打印
std::string& version
std::string&& version
这段代码基于之前提到的演讲中的一个示例。第10张幻灯片,从开头算起约在15:00处。
在完美转发中,使用std::forward将命名的右值引用t1和t2转换为未命名的右值引用。这样做的目的是什么?如果我们将t1和t2保留为左值,那么它将如何影响被调用的函数内部?
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
如果您在表达式中使用命名的右值引用,它实际上是一个左值(因为您通过名称引用对象)。考虑以下示例:void inner(int &, int &); // #1
void inner(int &&, int &&); // #2
现在,如果我们这样调用outer
outer(17,29);
我们希望将17和29转发到#2,因为它们是整数字面值,并且作为右值。但由于表达式中的t1和t2是左值,所以会调用#1而不是#2。这就是为什么我们需要使用std::forward将引用转换回未命名的引用。因此,在outer中,t1始终是左值表达式,而forward(t1)根据T1可能是右值表达式。后者只有在T1是左值引用时才是左值表达式。如果outer的第一个参数是左值表达式,则T1被推断为左值引用。static_cast<T&&>
也可以正确处理 const T&
。#include <iostream>
using namespace std;
void g(const int&)
{
cout << "const int&\n";
}
void g(int&)
{
cout << "int&\n";
}
void g(int&&)
{
cout << "int&&\n";
}
template <typename T>
void f(T&& a)
{
g(static_cast<T&&>(a));
}
int main()
{
cout << "f(1)\n";
f(1);
int a = 2;
cout << "f(a)\n";
f(a);
const int b = 3;
cout << "f(const b)\n";
f(b);
cout << "f(a * b)\n";
f(a * b);
}
生成:
f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&
值得强调的是,forward必须与具有转发/通用引用的外部方法配合使用。仅使用下面的语句作为forward是允许的,但除了造成混淆之外没有任何好处。标准委员会可能希望禁用这种灵活性,否则为什么不使用static_cast呢?
std::forward<int>(1);
std::forward<std::string>("Hello");
我认为,在引入r-value引用类型后,移动和前进是自然产生的设计模式。我们不应该假设一个方法被正确使用,除非禁止不正确的使用。
从另一个角度来看,在处理通用引用赋值中的rvalues时,保留变量的类型可能是可取的。例如:
auto&& x = 2; // x is int&&
auto&& y = x; // But y is int&
auto&& z = std::forward<decltype(x)>(x); // z is int&&
使用 std::forward
,我们确保 z
与 x
具有完全相同的类型。
此外,std::forward
不会影响左值引用:
int i;
auto&& x = i; // x is int&
auto&& y = x; // y is int&
auto&& z = std::forward<decltype(x)>(x); // z is int&
依然有z
与x
相同的类型。
因此,回到您的情况,如果内部函数有两个重载用于int&
和int&&
,您希望像赋值z
一样传递变量而不是y
。
示例中的类型可以通过以下方式进行评估:
std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;
x
是一个引用,那么std::forward<decltype(x)>(x)
可以简写为decltype(x)(x)
。 - HolyBlackCatstd::forward
。 - Sorush
std::forward<decltype(t1)>(t1)
或decltype(t1)(t1)
,请参见c++ - Perfect forwarding in a lambda? - Stack Overflow。 - user202729inner()
的可能原型有哪些? - undefined