关于右值引用与完美转发
今日的疑问: 在下面的代码中,forwardFunction(n); // 为什么这里调用的是process(int&),而不是process(int&&)?
#include <iostream>
#include <utility>
void process(int &x)
{
std::cout << "process(int& x):";
std::cout << "process called with lvalue: " << x << std::endl;
}
void process(int &&x)
{
std::cout << "process(int&& x):";
std::cout << "process called with rvalue: " << x << std::endl;
}
template <typename T>
void forwardFunction(T &&arg)
{
process(std::forward<T>(arg)); // 转发参数时保持原始值类别
}
int main()
{
int x = 10;
int &m = x; // 定义一个左值引用变量
int &&n = 10; // 定义一个右值引用变量
forwardFunction(m); // 调用 process(int&),传递一个左值引用
forwardFunction(n); // 为什么这里调用的是process(int&),而不是process(int&&)?
return 0;
}
解答:
在上面的代码中,forwardFunction(n) 调用的是 process(int&),而不是 process(int&&),
是因为: n 是一个右值引用变量,但它本身是一个左值。
右值引用变量可以绑定到一个右值,但它本身不是一个右值,而是一个具有名字的对象,可以被多次访问。
因此,当你将 n 传递给 forwardFunction 时,它会被推导为 int& 类型,而不是 int&& 类型。如果您想让 n 被当作右值传递,您需要使用 std::move(n) 来转换它。
std::forward 的作用是保持参数的原始值类别,即如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值1。这样可以实现完美转发,即根据参数的不同类型调用不同的重载函数。
想要使用右值的方法有很多,如下:
补充:
-
一个右值引用变量本身还是一个左值
-
经过测试:当我们把一个右值引用变量“直接传入”模板函数,调用的是左值引用;当我们把一个右值引用变量“通过std::forward()传入”模板函数,调用的是右值引用
template<typename T>
void construct(T *p,const T &val)//负责对象构造
{
new (p) T(val); //是左值引用
}
void construct(T *p,T &&val)
{
new (p) T(std::move(val));//是右值引用
}
//使用完美转发,就可以替换掉上面两个函数(也就是说下面这个函数,一个顶俩)
template<typename Ty>
void construct(Ty *p,Ty &&val)
{
new (p) Ty( std::forward<Ty>(val) );
}
- 左值:有内容有名字 ; 右值:没内存没名字(临时量)
- 右值引用或者常引用 可以引用右值
const int &c = 20;//常引用
int &&d = 20; //右值引用
- 关于cpp中的一个优化(重点!!!)
/*
cpp编译器对于对象构造的优化:用临时对象生产新对象的时候,
临时对象就不产生了,直接构造新对象就可以了(也就是说初始化的时候)
*/
class Test{};
Test t4 = Test(20);//和Test t4(20);没有区别
- 函数调用,将实参传给形参,是初始化还是赋值函数?
answer:是初始化.
既然是初始化,就要调用构造函数(或者拷贝构造)
Test GetObject(Test t)//3.Test(const Test &)//实参传给形参会调用拷贝构造
{
int val = t.getData();
Test tmp(val);//4.Test(Test &)
return tmp;//5.Test(const Test &)因为tmp是临时变量,所以无法带出GetObject这个函数,所以要创建一个临时变量,所以以tmp为原型再拷贝构造一个临时变量
}//6.~Test() tmp进行析构
//7.~Test() 形参t要析构
int main()
{
Test t1;//1. Test(int)
Test t2;//t2.Test(int)
t2.GetObject(t1);//8.operator=
//9.~Test()临时变量要析构
//10.~Test()t1析构
//11.~Test()t2析构
return 0;
}
- 举个例子,大家想想下面的代码中,构造与析构的顺序是什么?
- 例子一
Test Getobject(Test &t)//因为是引用传递,所以没有调用拷贝构造
{
int val = t.getData();
/*Test tmp(val)
return tmp;*/
return Test(val);//返回临时对象
}
int main()
{
Test t1;
Test t2;
t2 = Getobject(t1);//临时对象在赋值给t2后,调用析构函数(不是初始化)
return 0;
}
1.构造
2.构造
3.拷贝构造(临时对象)
4.赋值
5.析构(临时对象)
6.析构
7.析构
- 例子二
Test Getobject(Test &t)
{
int val = t.getData();
/*Test tmp(val)
return tmp;*/
return Test(val);//返回临时对象
}
int main()
{
Test t1;
Test t2 = Getobject(t1);//用临时对象在初始化新的对象,临时对象就会被优化掉(是初始化)
return 0;
}
1.构造
2.构造(直接构造t2对象,没有调用拷贝构造了【或者说拷贝构造被优化了!!!】)
3.析构
4.析构
下面是一些大佬总结的《cpp对象优化的三条原则》
1.函数参数传递过程中,对象优化按引用传递,不要值传递
2.函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
3.接受返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收