本文假定,我们已经明白左值和右值的区别。(可以认为:无法取地址的值,是左值;可以取地址的值,是右值;)
本文的目标是,搞懂std::move(移动)和std::forward(完美转发)的原理。
本文并不是一个很好的介绍文章,它的逻辑衔接不是很好。但是,每一节的开头都给出了参考链接,可以自行阅读。
本文是一个学习总结:搞懂模板参数的推断;move和forward只是,模板函数的参数是右值引用的应用。
本文包含的内容:模板参数推断总结;std::move源码分析;std::forward源码分析;
参考自:
fx(i) | fx(ci) | fx(5) | |
---|---|---|---|
从左值引用函数参数推断类型 | |||
template |
i是一个int;模板参数类型T是int | ci是一个const int;模板参数类型T是const int | 错误:传递给一个&参数的实参必须时一个左值 |
template |
i是一个int;模板参数类型T是int | ci是一个const int;模板参数类型T是int | 一个const &参数可以绑定到一个右值;T是int |
从右值引用函数参数推断类型 | |||
template |
i是一个int;模板参数类型T是int&,这是一个例外规则; 接着触发第二个列外,引用折叠,等效为f3(int &) |
ci是一个const int;模板参数类型T是const int&; 同左边,最后等效为f3(const int&) |
当然可以传递一个右值;模板参数类型T是int |
普通的函数参数推断类型 | |||
template |
i是一个int;模板参数类型T是int | ci是一个const int;模板参数类型T是int | 模板参数类型T是int |
template void f(T &P);
),绑定规则告诉我们,只能给它传递一个左值。template void f2(const T &P);
),可以给它传递任何类型的实参–一个对象(const或非const)、一个临时对象,一个字面常量值。由于函数模板参数本身包含const,T的类型不会推断为const类型,const已经是函数参数类型的一部分了。template void f3(T &&P);
),当然可以给它传递一个右值。通常情况下,我们不能将一个右值引用绑定到一个左值上。这里有两个例外。(这两个例外是后面std::move和std::forward的基础。或者是为了实现move和forward,添加了这两个例外)
X& &、X& &&、X&& &都折叠成X&; 类型X&& && 折叠成X&&
)template void f4(T P);
) ,采用值传递的方式。这意为着无论传递的是什么类型,都进行拷贝,形成一个新的对象。(传入int&, T类型也是int)int &
这种情况。因为这种情况,通过上面两个例外,会基本等同于上表。参考自:
这个函数的作用是,将一个右值引用绑定到一个左值上。(将任意类型(通常是左值),转换成右值类型。)这样做的好处是,可以避免重复的拷贝。
例如,int rr1 = 1; int &&rr3 = std::move(rr1)
。调用std::move就意味着承诺:除了对rr1赋值和销毁它之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。(我们可以销毁一个移后源对象,也可以给它重新赋值,但不能使用一个移动后源对象的值。)
下面是一个move的一个使用示例。
#include
#include
#include
#include
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
// 使用 push_back(const T&) 重载,
// 表示我们将带来复制 str 的成本
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
// 使用右值引用 push_back(T&&) 重载,
// 表示不复制字符串;而是
// str 的内容被移动进 vector
// 这个开销比较低,但也意味着 str 现在可能为空。
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
输出如下:
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
此时,我们看下libstdc++: move.h Source File 的源码。分析下,为什么std::move,无论接收一个左值还是右值,都可以将其转换成右值。
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
下面内容基本是来自《C++ primer》,书中解释的已经非常清晰了。
我们分析下面的函数过程。
string s1("hi"), s2;
s2 = std::move(string("bye")); // 传入一个右值
s2 = std::move(s1); // 传入一个左值
我们先来看下s2 = std::move(string("bye"));
。move是一个参数为右值引用的模板函数,根据最上面的表格,我们可以得到下面内容:
string
类型。(T表示上面的_Tp
,后文皆是如此)remove_reference
使用string进行实例化。typename std::remove_reference<_Tp>::type&&
,即使string&&
。(使用typename是为了区别type是类型,而不是static成员变量;remove_reference作用是去除引用;remove_reference的type是去除引用后的类型)string&&
。static_cast(string&&)
,正常运行。我们再来考虑第二个赋值语句s2 = std::move(s1);
。根据最上面的表格,我们可以得到下面内容:
string&
。remove_reference
使用string&
进行实例化。remove_reference<_Tp>::type
的成员是string
。string&&
。__t
的类型,由于引用折叠,~~从T& &&
~~折叠成T&
。static_cast(string&)
,则是将一个左值转换成右值。(更严谨些是,将左值转换成亡值–右值的一种。移后源之后不可用,可以销毁或者重新赋值。书上是这么说的,”将一个右值引用绑定到一个左值的特性允许它们截断左值,并且这种截断是安全的“。至于什么是”安全的截断“,不知道。)参考自:
下面是cppreference中完美转发(forward)的示例。可见,传入的类型在完美转发后保持不变:
#include
#include
#include
struct A {
A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
A(int& n) { std::cout << "lvalue overload, n=" << n << "\n"; }
};
class B {
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{
}
private:
A a1_, a2_, a3_;
};
template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
template<class T, class... U>
std::unique_ptr<T> make_unique2(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}
int main()
{
auto p1 = make_unique1<A>(2); // 右值
int i = 1;
auto p2 = make_unique1<A>(i); // 左值
std::cout << "B\n";
auto t = make_unique2<B>(2, i, 3);
}
输出:
rvalue overload, n=2
lvalue overload, n=1
B
rvalue overload, n=2
lvalue overload, n=1
rvalue overload, n=3
我们来看下std::forward的源码:
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
*
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
我没搞明白,为什么要分成两个函数。不过,最核心的是static_cast<_Tp&&>(__t)
。下面内容基本是翻译自上面的stackoverflow链接。
基本上,给定表达式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 和非常量的所有组合:
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 个论点需要 2 N 2^N 2N个组合,这是一场噩梦。我们希望自动执行此操作。(这实际上是我们让编译器在 C++11 中为我们做的事情。)
解决方案是改用新添加的rvalue-references;我们可以在推导右值引用类型时引入新规则并创建任何想要的结果。(新规则,即是上表中的两个例外)
在代码中:
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce(int& &&) -> deduce(int&)
deduce(1); // deduce(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);
把这些放在一起给了我们“完美转发”:
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}