先谈谈引用,毕竟这个是基础,如果这里理解有误区,本节内容肯定消化不了。下面是我在C++右值与右值引用_李兆龙的博客的博客-CSDN博客复制下来的帖子放在下面了(下面的第一部分是我复制的):
第一部分:先说说引用
引言
C++11中最重要的特性之一就是移动,这在大多数情况下可以大幅度的提升程序的性能,其实也不难理解,举个简单的例子,vector中当容量到达提前分配的最大值时会进行重新分配内存,那对于所有原本vector中所有的元素来说,要重新拷贝一份到新的分配的内存空间,那如果vector中提前数据较大,拷贝旧的数据,那是多大的内存开销啊。在C++11之前确实需要这样,但在移动这个新特性出现以后,一切都不一样了,在上面这个例子里,我们根本不需要拷贝再析构一份数据,只需要将控制权从原来的内存中转移到新的内存中即可,这便大幅度的提升了程序的性能。但要彻底清楚移动,我们首先来看看右值与右值引用。
左值与右值
我们平时遇到的表达式要么是左值,要么是右值,右值在C++98时就指纯右值,即就是临时变量,比如值传递的时拷贝的参数或者函数返回值是一个值时的临时拷贝对象,或者单纯的字面值 比如 int a = 5 中的5就是一个右值,而在C++11中为了支持移动,又引入了亡值,通常在移动后,移后源对象就会被调用析构函数,因为其中的值已经被转移,亡值这个名称也可以说是很通俗易懂了 比如std::move函数的返回值就是亡值 我们先来看一段代码来理解什么是亡值
int main()
{
string a="hello";
vector aa={1,2,3};
cout << &aa[2] << endl;
vector>vec;
vec.push_back(std::move(aa));
cout << vec[0][2] << endl;
cout << &vec[0][2] << endl;
cout << aa.size() << endl;
}
输出结果:
0x55750d91be78
3
0x55750d91be78
0
在这里aa在std::move以后就成了亡值 其对象中的数据被vec“盗”走 在调用结束后aa被析构
所以我们可以对左值和右值做一个区分:
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(亡值或纯右值)。:
右值引用
C++11中,为了支持移动 *** 作,便引入了一种新的引用类型 右值引用 右值引用可以让我们避免不必要的拷贝,从而大幅度提升程序的性能,可以用来实现高效的库。
右值引用在语法上与普通的引用类似 左值引用就是类型后加一个&,右值引用就是加两个&,值得一提的是函数参数为左值引用,其实参可以是左值和右值,而函数参数是右值,实参只能是右值,这就引出了一个函数参数匹配的问题,左值引用既然兼容左值与右值,我们怎么让编译器知道何时为右值从而做出正确的处理呢,
StrVec(const StrVec&);
StrVec(StrVec&&) noexcept;
我们通常给左值引用加上const 这时对于右值来说显然参数为 && 的函数才是最优匹配,而对左值来说参数为const &的函数是最优匹配 这就解决了这个问题。
没有移动构造函数 右值也会被拷贝
引入右值引用就是为了参数为右值时把原来的拷贝替换为移动,从而提升程序性能,但如果我们仅是把参数换为右值,而没有相应的移动构造函数,我们的右值会向下例一样,所以我们在程序程序编写时,不但要注意参数问题,在进行实际 *** 作的时候也要针对不同的参数做出不同的应对
void foo(X&& x)
{
X anotherX = x;
}
那对于这个函数来说真的让程序的性能有所提示吗,答案当然是否定的,我们对于右值的判断先前是是否有名字,但这里好像是个例外,它既有名字,同时也是个右值,我们虽然清楚,但编译器并不知道,所以需要我们手动的使它变成一个右值,而我们前面提到了一个函数,就是std::move,它可以使一个对象变成一个亡值,这时在参数匹配时就会匹配 foo(&&) 而不是之前的foo(const &)了,这样也就达到了我们的目的,减少一次拷贝,提升程序性能。这是一个值得注意的点 不然我们只会以为性能提升,但实则任进行了拷贝。
void foo(X&& x)
{
X anotherX = std::move(x);
}
这样才是正确的处理方式。
完美转发
这个问题是在大佬博客中发现的问题,顿时觉得C++博大精深,遂放在此篇博客中进行讨论,原问题链接在文末,侵删
什么是完美转发,就是通过引用折叠从而在接受一个参数时能够适用于所有的情况,包括右值与左值。
引用折叠
&& & -》 &
& && -》 &
& & -》 &
&& && —》 &&
template
void exec(fun f,a tmpa,b tmpb){
f(a,b);
}
这样我们可以满足所有的情况 但是每次都有一次不必要的拷贝,你也许会说使用引用,没错,我们来看看
template
void exec(fun f,a &tmpa,b &tmpb){
f(tmpa,tmpb);
}
int tmp(int a,int b){
cout << a*b << endl;
}
int main()
{
int a=5;
int b=6;
exec(tmp,a,b);
}
没有问题 编译正常,但是这就引来了一个问题,第一个版本与第二个版本看似相同,实则天差地别,因为第二个版本不能以右值作为参数 我们这样就会报错
exec(tmp,a,6);
报错原因是这样的
:cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
我们不能够把一个右值绑定到一个左值引用上
你也许会想到上面提到的引用叠加 我们可以这样解决这个问题
template
void exec(fun f,a &&tmpa,b &&tmpb){
f(tmpa,tmpb);
}
int tmp(int a,int b){
cout << a*b << endl;
}
int main()
{
int a=5;
int b=6;
exec(tmp,a,6);
}
在提一个问题 就是当我们要使用这个函数时呢
int tmp(int &&a,int &&b){
cout << a*b << endl;
}
我们会发现一般的调用都会失败 会报这样的错
cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
原因在于引用合并后T1和T2的的类型便变成了int,int 与 int**当然是无法被绑定的。
所以我们需要一个函数 std::forword() 这个c++11中的函数可以回复模板参数类型变化的问题
所以这样修改下就可以了
template
void exec(fun f,a &&tmpa,b &&tmpb){
f(std::forward(tmpa),std::forward(tmpb));
}
int tmp(int &&a,int &&b){
cout << a*b << endl;
}
int main()
{
int a=5;
int b=6;
exec(tmp,std::move(a),std::move(b));
}
std::move函数的使用时机需要斟酌
因为move函数的作用是把一个左值转化成一个亡值 这意味它的值已经被“盗” ,而我们不能除了赋值和销毁外对其做任何假设,所以意味着正确的使用会使得程序性能大幅度提升,而错误的使用会使的程序出现莫名其妙的错误。所以只有我们确定移动 *** 作是安全的 时候才可以使用。
以上就是我在学习过之后的感触 希望对有同样问题的你有所帮助。
参考资料:
C++ primer plus
C++ primer
以上就是我从别的网站上复制的内容。下面讲讲模板实参的推断和引用
第二部分:模板实参的推断和引用
先看一个例子:
template
void f3(T && t){}
前提说明:
如果这样调用 f3(4) 是不合法的,不能将一个右值绑定到左值上。但是c++语言在正常的绑定规则之外有2个例外(这2个例外是move这种标准库设施正常工作的基础):
(1)当我们将一个左值(比如i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如果T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时(i是一个int类型),编译器推断T的类型是int& 而不是int。T被推断为int&看起来好像f3的函数参数应该是一个类型int& 的右值引用。通常,我们不能(直接)定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的。
例如下面一段代码:
#include
#include
using namespace std;
void f(int &v1,int &v2)
{
cout << "更改前 v1 "<
/*注:t1是一个指向模板类型参数的右值引用类型*/
/*注:t2是一个指向模板类型参数的普通类型*/
//无论什么时候,实际参数的值都会被拷贝到t2中,T2 会丢掉实际参数的const和引用特性。
void func(F f,T1 &&t1,T2 t2)
{
int a1 = 1;
T1 pa1 = a1; //注意,t1被传进来如果是左值,T1就是一个左值引用类型。改变pa1就会改变a1
++ pa1;
int a2 = 1;
T2 pa2 = a2; //不论t2传进来什么,T2都不会是一个引用类型的
++ pa2;
cout << "a1:" << a1 << endl;
cout << "a2:" << a2 << endl;
f(t1,t2);
cout << "更改后 " << "t1 "<< t1 <<" t2 "<
注意:理解引用折叠之前,必须先理解引用折叠的条件:必须把左值传递给指向模板类型参数的右值引用。
输入结果:
a1:2
a2:1
更改前 v1 123 v2 23
更改后 t1 124 t2 24
i 124 j23
举例:执行func(f,i,j);首先模板func实参t1的类型是int,是一个左值,然后因为给一个指向模板类型参数T1的 右值引用类型赋值,所以T1被推断为int&类型,所以t1最后的类型是int& &&,经过引用折叠后,t1的类型是int&类型,t1绑定到i,模板内如果改变t1,那么就会改变实际参数i。是不是很奇妙。
(2)我们可以间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则则拓展到右值引用。只在一种特殊的情况下会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型X:
X& &、X& && 、X&& & 都折叠成类型X&
类型X&& && 折叠成X&&
例:如果将引用折叠和右值引用的特殊类型推断组合在一起,则意味着我们可以对一个左值调用f3。当我们将一个左值传递给f3的(右值引用)函数参数时,编译器推断T为一个左值引用类型:
f3(i); //实参是一个int值;模板参数T是int&
f3(ci); //实参是一个左值;模板参数T是一个const int&
当一个模板参数被推断为引用类型时,折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。例如,f3(i)的实例化结果可能像下面这样:
//无效代码,只用于演示目的
void f3(int& &&); //当T是int& 类型时,函数参数为int& &&
f3的参数是T&&且T是int&,因此T&&是int& &&,会折叠成int& 。因此即时f3的函数参数形式是一个右值引用(即,T&&),此调用也会用一个左值引用类型(即,int&)实例化f3:
void f3(int &); //当T是int& 时,函数参数折叠为int&
这两个规则导致了下面重要的结果:
(1)如果一个函数参数是一个指向模板类型参数的右值引用(如果,T&&),则它可以被绑定到一个左值;且
(2)如果实参是一个左值,则推断出模板实参类型是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。
另外值得注意的是,这两个规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。对于这种类型参数,(显然)可以传递给它右值,而如我们刚刚看到的,也可以传递给它左值。
编写接受右值引用参数的模板函数模板参数可以推断为一个引用类型,这一特性对模板内的代码可能有令人惊讶的影响:
template void f3(T&& val)
{
T t = val; //拷贝还是绑定一个引用?
t = fcn(t); //赋值仅改变t还是既改变t又改变val?
if(val == t) {/* ... */} //若T是引用类型,则一直为true
}
当我们对一直右值调用f3时,例如字面常量42,T为int。在此情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化。当我们对t赋值时,参数val保持不变。
另一方面,当我们对于一个左值i调用f3时,则T为int&,当我们定义并初始化局部变量t时,赋予它的类型int&.因此,对t的初始化将绑定到val。当我们对t赋值时,也同时改变了val的值。在f3的这个实例化版本中,if判断永远得到true。
当代码中涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就变得异常困难(虽然remove_reference这样的类型转换可能会有帮助)。
在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。我们将在16.2.7(第612页)介绍实参转发。在1 6.3节介绍模板重载。
目前应该注意的是,使用右值引用的函数模板通常使用我们在13.6.3(481页)节中看到的方式进行重载:
template void f(T&& ); //绑定到非const 右值
template void f(const T&); //左值和const右值
第三部分:remove_reference
在介绍remove_reference之前,我们先看一段代码
#include
#include
using namespace std;
int main()
{
int a[] = {1,2,3,4,5,6};
decltype(*a) b = a[1];
b = 1234;
cout << a[1] << endl;
return 0;
}
运行结果:1234
输出为1234,因为decltype(*a)返回*a的类型,我们知道*a的类型实际上是int& ,所以此时修改a[0] 等同于修改了b。
int main()
{
int a[] = {1,2,3};
remove_reference::type b = a[0];
a[0] = 4;
cout << b; //输出1
return 0;
}
看看remove_reference 的做了什么
他封装了一个普通的模板类,并且typedef T type,主要看第二个,封装了一个引用类型的T&
我们使用时remove_reference
remove_reference
下面看看cplusplus中对remove_reference的描述:
std::remove_reference
template
Remove reference
Obtains the non-reference type to which T refers.
翻译:获取T引用的非引用类型
The transformed type is aliased (别名) as member type remove_reference::type.
翻译:转换之后的类型作为remove_reference::type的成员类型
If T is a reference type (either lvalue reference or rvalue reference), this is the type to which it refers. Otherwise, it is the same as T, unchanged.
翻译:如果T是引用类型(左值引用或右值引用),则这就是它所引用的类型。否则,与T相同,不变。
Notice that this class merely obtains a type using another type as model, but it does not transform values or objects between those types.
翻译:请注意,此类仅使用另一个类型作为模型来获取类型,但它不会在这些类型之间转换值或对象。
Template parametersT
A type.
Member types
member type | definition |
---|---|
type | If T is a reference type, the type referrered to by T. Otherwise, T. |
翻译:如果T是一个引用类型,类型是T所引用的类型;否则,T
Example// remove_reference
#include
#include
int main()
{
typedef int&& rval_int;
typedef std::remove_reference::type A;
typedef std::remove_reference::type B;
typedef std::remove_reference::type C;
typedef std::remove_reference::type D;
std::cout << std::boolalpha;
std::cout << "typedefs of int:" << std::endl;
std::cout << "A: " << std::is_same::value << std::endl;
std::cout << "B: " << std::is_same::value << std::endl;
std::cout << "C: " << std::is_same::value << std::endl;
std::cout << "D: " << std::is_same::value << std::endl;
return 0; }
Output:
typedefs of int:
A: true
B: true
C: true
D: true
说明:is_same::value,是一个模板,接受A,B两个参数,均为类型,返回bool值,如果A,B表示同一种类型,返回true,否则,返回false。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)