函数模板可以将自己的参数完美的转发给内部调用的其他函数。
完美指的是不仅能准确转发参数的值,还能保证被转发具体的左值或右值属性不变。
借用万能引用,通过引用的方式接受左右属性的值。
引用折叠规则:参数为左值或左值引用,T&& 将转化为int &
参数为左值或左值引用,T&& 将转化为int &&
std::forward<T>(v) : T为左值引用,v将转化为T类型的左值
T为右值引用,v将转化为T类型的右值
[C++特性]对std::move和std::forward的理解 - 知乎 (zhihu.com)
下面内容来自上面链接
左值、右值、左值引用以及右值引用
std::move和std::forward这两个API主要服务于左值引用和右值引用的转化和转发,因此再了解这两个API之前,需要先弄清楚这几个概念。
- 左值:一般指的是在内存中有对应的存储单元的值,最常见的就是程序中创建的变量
- 右值:和左值相反,一般指的是没有对应存储单元的值(寄存器中的立即数,中间结果等),例如一个常量,或者表达式计算的临时变量
int x = 10
int y = 20
int z = x + y
//x, y , z 是左值
//10 , 20,x + y 是右值,因为它们在完成赋值操作后即消失,没有占用任何资源
- 左值引用:C++中采用 &对变量进行引用,这种常规的引用就是左值引用
- 右值引用:这个概念实际上不是说对上述的右值进行引用(因为右值本身也没有对应的存储单元),右值引用实际上只是一个逻辑上的概念,最大的作用就是让一个左值达到类似右值的效果(下面程序举例),让变量之间的转移更符合“语义上的转移”,以减少转移之间多次拷贝的开销。右值引用符号是&&。
例如,对于以下程序,我们要将字符串放到vector中,且我们后续的代码中不再用到x:
std::vector<std::string> vec;
std::string x = "abcd";
vec.push_back(x);
std::cout<<"x: "<<x<<"\n";
std::cout<<"vector: "<< vec[0]<<"\n";
//-------------output------------------
// x: abcd
// vector: abcd
该程序在真正执行的过程中,实际上是复制了一份字符串x,将其放在vector中,这其中多了一个拷贝的开销和内存上的开销。但如果x以及没有作用了,我们希望做到的是 真正的转移,即x指向的字符串移动到vector中,不需要额外的内存开销和拷贝开销。因此我们希望让变量 x传入到push_back 表现的像一个右值 ,这个时候就体现右值引用的作用,只需要将x
的右值引用传入就可以。
std::move
前面提到了右值引用的主要作用是减少不必要的拷贝开销和内存开销。而std::move的作用就是进行无条件转化,任何的左值/右值通过std::move都转化为右值引用。将上面的程序改写成右值引用的方式
std::vector<std::string> vec;
std::string x = "abcd";
vec.push_back(std::move(x));
std::cout<<"x: "<<x<<"\n";
std::cout<<"vector: "<< vec[0]<<"\n";
//-------------output------------------
// x:
// vector: abcd
可以看到,完成`push_back`后x
是空的。
使用场景
对于一个值(比如数组、字符串、对象等)如果在执行某个操作后不再使用,那么这个值就叫做将亡值(Expiring Value),因此对于本次操作我们就没必要对该值进行额外的拷贝操作,而是希望直接转移,尽可能减少额外的拷贝开销,操作后该值也不再占用额外的资源,此时就可以使用std::move。
举个例子
#include <iostream>
#include <vector>
#include <string>
class A {
public:
A(){}
A(size_t size): size(size), array((int*) malloc(size)) {
std::cout
<< "create Array,memory at: "
<< array << std::endl;
}
~A() {
free(array);
}
A(A &&a) : array(a.array), size(a.size) {
a.array = nullptr;
std::cout
<< "Array moved, memory at: "
<< array
<< std::endl;
}
A(A &a) : size(a.size) {
array = (int*) malloc(a.size);
for(int i = 0;i < a.size;i++)
array[i] = a.array[i];
std::cout
<< "Array copied, memory at: "
<< array << std::endl;
}
size_t size;
int *array;
};
int main() {
std::vector<A> vec;
A a = A(10);
vec.push_back(a);
return 0;
}
//----------------output--------------------
// create Array,memory at: 0x600002a28030 // A a = A(10); 调用了 构造函数A(size_t size){}
// Array copied, memory at: 0x600002a28050 //vec push的时候拷贝一份,调用构造函数A(A &a){}
从输出可以看到,每次进行push_back的时候,会重新创建一个对象,调用了左值引用A(A &a) : size(a.size)
对应的构造函数,将对象中的数组重新深拷贝一份,如果对象占用内存大,并且该对象此时已经是一个将亡值,那么这样带来了许多不必要的开销,降低了程序的性能。这个时候就可以用右值引用进行优化,避免拷贝的开销
int main () {
std::vector<A> vec;
A a = A(10);
vec.push_back(std::move(a));
return 0;
}
//----------------output--------------------
// create Array,memory at: 0x600003a84030
// Array moved, memory at: 0x600003a84030
可以看到,这个时候虽然也重新创建了一个对象,但是调用的是这个构造函数A(A &&a) : array(a.array), size(a.size)
(这种采用右值引用作为参数的构造函数又称作移动构造函数),此时不需要额外的拷贝操作,也不需要新分配内存。
std::forward
std::forward的作用是完美转发,如果传递的是左值转发的就是左值引用,传递的是右值转发的就是右值引用。
在具体介绍std::forward之前,需要先了解C++的引用折叠规则,对于一个值引用的引用最终都会被折叠成左值引用或者右值引用。
- T& & -> T& (对左值引用的左值引用是左值引用)
- T& && -> T& (对左值引用的右值引用是左值引用)
- T&& & ->T& (对右值引用的左值引用是左值引用)
- T&& && ->T&& (对右值引用的右值引用是右值引用)
只有对于右值引用的右值引用折叠完还是右值引用,其他都会被折叠成左值引用,根据折叠规则,可以构造出一个通用引用。
#include<iostream>
template <typename T>
void foo(T&& param){
if(std::is_rvalue_reference<decltype(param)>::value)
std::cout<<"rvalue reference\n";
else std::cout<<"lvalue reference\n";
}
int main(){
int a = 0;
foo(a);
foo(std::move(a));
return 0;
}
//------------output----------
// lvalue reference
// rvalue reference
foo(a)
,T就是int &,则param的类型为T&&->int & &&->int &foo(std::move(a))
,std::move转成右值引用,那么T就是int&&,则param的类型为T &&->int && &&->int &&
前面提到的std::move可以减少不必要的拷贝开销,可以提高程序的效率,但是std::forward的作用是转发,左值引用转发成左值引用,右值引用还是右值引用,刚开始一直想不通这个API的意义到底是什么?
原来是在程序的执行过程中,对于引用的传递实际上会有额外的隐式的转化,一个右值引用参数经过函数的调用转发可能会转化成左值引用,但这就不是我们希望看到的结果。
在上面的程序上进行修改
#include <iostream>
#include <vector>
#include <string>
class A {
public:
A(){}
A(size_t size): size(size), array((int*) malloc(size)) {
std::cout
<< "create Array,memory at: "
<< array << std::endl;
}
~A() {
free(array);
}
A(A &&a) : array(a.array), size(a.size) {
a.array = nullptr;
std::cout
<< "Array moved, memory at: "
<< array
<< std::endl;
}
A(A &a) : size(a.size) {
array = (int*) malloc(a.size);
for(int i = 0;i < a.size;i++)
array[i] = a.array[i];
std::cout
<< "Array copied, memory at: "
<< array << std::endl;
}
size_t size;
int *array;
};
template<typename T>
void warp(T&& param) {
if(std::is_rvalue_reference<decltype(param)>::value){
std::cout<<"param is rvalue reference\n";
}
else std::cout<<"param is lvalue reference\n";
A y = A(param);
A z = A(std::forward<T>(param));
}
int main(){
A a = A(100);
warp(std::move(a));
return 0;
}
//----------------output----------------
// create Array,memory at: 0x600002e60000 //main函数中,A a = A(100);调用构造函数
// param is rvalue reference //使用了std::move,根据引用折叠规则,param是一个右值引用
// Array copied, memory at: 0x600002e60070 // A y = A(param); 可以看到调用的是拷贝的构造函数
// Array moved, memory at: 0x600002e60000。// A z = A(std::forward<T>(param)); 调用了移动构造函数
从程序的输出就可以看到,当一个右值引用再进行转发的时候,没使用std::forward进行二次转发的时候,实际上是会被隐式的转换,转发成一个左值引用,从而调用不符合期待的构造函数,带来额外的开销,所以std::forward的一个重要作用就是完美转发,确保转发过程中引用的类型不发生任何改变,左值引用转发后一定还是左值引用,右值引用转发后一定还是右值引用!