在C++11之前,一个变量分为左值和右值:左值是可以放在=
运算符左边的值,有名字,可以用&
运算符取地址(如 int n = 10;
n即为左值);右值则是只能放在=
运算符右边,没有名字,不能用&
运算符取地址的值,一般是临时变量(非引用返回的函数返回值、表达式等,例如函数int func()
的返回值,表达式a+b
的返回值)、lambda表达式、不跟对象关联的字面量值,例如true,100等。
C++11以后对C++98中的右值进行了扩充,在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于C++98标准中右值的概念;将亡值则是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象,比如返回右值引用T&&
的函数返回值、std::move()
的返回值,或者转换为T&&
的类型转换函数的返回值(如static_cast<int&&>(10)
)。
//左值
int a = 10;
int b = a;
++a; //前置自增/自减为左值
--a;
"abcdefg"; //注意常量字符串为左值,可以对其取地址
//右值
a++; //后置自增/自减为右值
a--;
a + b;
100; //其他的常量类型为右值
5.0;
右值引用是c++11中新加入的类型,主要作用是减少对象复制时不必要的内存拷贝,以实现对象之间的快速复制:对于函数按照值返回的形式,必须创建一个临时对象,临时对象在创建好之后,则进行了一次拷贝构造,再将该临时对象拷贝构造给接收的对象,则又进行了一次拷贝构造,此时临时对象被销毁。就相当于返回一个临时对象时,会调用两次拷贝构造,对空间而言是一种浪费,程序的效率也会降低,并且临时对象的作用不是很大。
左值引用和右值引用都属于引用类型,都必须在声明时进行初始化,而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
一般情况下,左值引用只能接受左值对其进行初始化,右值引用只能接受右值对其进行初始化;但常左值引用是个例外,它是“万能”的引用类型:它可以接受非常量左值、常量左值、右值对其进行初始化,不过只能通过引用来读取数据,无法去修改数据,因为其被const
修饰。在c++11以后,右值在函数参数匹配时会优先与右值引用绑定,而不是const
左值引用。
注意:
const
左值引用,只读)绑定到一个临时变量时,本来会被销毁的临时变量的生存期会延长至这个引用的生存期。T
的对象t:如果t为左值,那么t的非静态成员也为左值,如果t为右值,那么t的非静态成员也为右值。int a = 10, b = 30;
int& n = a; //左值引用
int& n = 10; //错误,编译不过
int&& m = 100; //右值引用
m = m + 50; //m = 150
const int& k = 100; //这样也行,但是k只读,不能修改
int&& m = a + b; //m为右值引用
//以下函数返回一个将亡值(右值)
constexpr int&& func(int& _Arg)
{
return (static_cast<int&&>(_Arg));
}
std::string func()
{
return std::string("XXXX");
}
//func()返回的临时变量的生命周期会延长至pStr的生命周期
std::string&& pStr = func();
std::cout << "pStr : " << pStr << std::endl;
pStr = "OOOO";
std::cout << "pStr : " << pStr << std::endl;
输出结果为
pStr : XXXX
pStr : OOOO
不要返回局部变量的引用
const std::string& func1()
{
return static_cast<std::string&>(std::string("XXXX"));
}
std::string&& func2()
{
return static_cast<std::string&&>(std::string("XXXX"));
}
int main()
{
const std::string& aa = func1();
std::string&& aa2 = func2();
//std::cout << "aa: " << aa << std::endl; 会报错,因为aa引用的对象已销毁
//std::cout << "aa2: " << aa2 << std::endl; 会报错,因为aa2引用的对象已销毁
return 0;
}
对于一个实际的类型 T
,它的左值引用是 T&
,右值引用是 T&&
。
那么:是不是看到 T&
就一定是个左值引用?是不是看到 T&&
就一定是个右值引用?
对于前者的回答是 “是”,对于后者的回答为 “否”:
T&&
,其中T
是需要被推导的类型,那这个变量或者参数就不一定是右值引用,而是一个万能引用(universal reference),如template<typename T> void foo(T&& param);
和auto&& var2 = var1;
。万能引用不是一种引用类型,而是会根据T
的推导结果,决定其究竟是一个左值引用还是右值引用。T&&
,不能携带任何的修饰符:如 const T&&
,std::vector<T>&&
等为右值引用。#include <string>
#include <iostream>
#include <type_traits>
template <typename T>
void mytest(T&& t)
{
//判断T是否为引用类型
std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
std::cout << "t: " << t << std::endl;
}
int main()
{
int a = 11;
mytest(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
mytest(22);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&
std::cout << std::endl;
int& m = a;
int&& n = 22;
n = n + 10;//右值引用可以修改绑定对象的值
mytest(m);//此时T&& t接收到的实参为左值(m本身为左值),T被推导为int&,t的类型为int& &&,引用折叠为int&
mytest(n);//此时T&& t接收到的实参为左值(n本身为左值),T被推导为int&,t的类型为int& &&,引用折叠为int&
std::cout << std::endl;
mytest(static_cast<int&>(m));//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
mytest(static_cast<int&&>(n));//此时T&& t接收到的实参为右值,T被推导为int,t的类型为int&&
std::cout << std::endl;
return 0;
}
运行结果如图:
#include <string>
#include <iostream>
#include <type_traits>
template <typename T>
void mytest2(T&& t)
{
std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
std::cout << "is_const: " << std::is_const<T>::value << std::endl;
std::cout << "t: " << t << std::endl;
}
int main()
{
const int a = 11;
mytest2(a);
mytest2(10);
return 0;
}
运行结果如图:
我们发现我们传进去的const
的修饰符也消失了,但实际上并不是如此,这里就要有顶层const
和底层const
的区别了,引用是只有底层const
的,不存在顶层const
,而is_const
这个方法它只能检测到顶层const
,所以输出了false
;这里的T
应该为const int&
。
//这里T&&并不是万能引用,因为T的类型在类模板实例化时已经确定,调用函数void fun(T&& t)时,T已为确定类型,不用再推导
typemplate <typename T>
class A
{
void func(T&& t);
}
//但如下是万能引用
typemplate <typename T>
class A
{
typemplate <typename U>
void func(U&& u);
}
引用折叠只能应用于推导的语境下(如:模板实例化,auto
,decltype
等),其他情况非法(如 int& & n = a;
)。
右值引用加上右值引用等于右值引用,除了这个外其他的所有形式都为左值引用:
T& &
---> T&
T& &&
---> T&
T&& &
---> T&
T&& &&
---> T&&
对于template <typename T> void func (T& t)
形式 (即T&
),不管T
是什么类型,T&
都会变成T&
:
int& &
---> int&
int&& &
---> int&
int &
---> int&
因此这个函数模板只能接收左值实参。
对于template <typename T> void func (T&& t)
形式 (即T&&
),如果 T
被显示指定为int&&
,那 T&&
的结果仍然是右值引用,int&& &&
折叠成了 int&&
;
如果T
是根据实参推导时:
若实参为左值, T
推导出是左值引用(如int&
),T&&
的结果仍然是左值引用,int& &&
折叠成了 int&
;
若实参为右值, T
推导出是实际类型(如int
), T&&
的结果仍然是右值引用int&&
。
template <typename T>
void mytest2(T&& t)
{
std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
std::cout << "t: " << t << std::endl;
}
int main()
{
int a = 11;
mytest2(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&
mytest2(10);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&
mytest2<int&&>(static_cast<int&&>(30));//此时T&& t接收到的实参为右值,T被显示指定为int&&,t的类型为int&& &&,引用折叠为int&&
return 0;
}
运行结果如图:
首先解释一下什么是完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
#include <string>
#include <iostream>
#include <type_traits>
void func(int& a)
{
std::cout << "lvalue = " << a << std::endl;
}
void func(int&& a)
{
std::cout << "rvalue = " << a << std::endl;
}
template <typename T>
void mytest2(T&& t)
{
std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
func(std::forward<T>(t));
//func(t);
}
int main()
{
int a = 11;
mytest2(a);//此时T&& t接收到的实参为左值,T被推导为int&,t的类型为int& &&,引用折叠为int&,func()接收到的参数为int&
std::cout << std::endl;
mytest2(10);//此时T&& t接收到的实参为右值,T被推导为int(并非int&&),t的类型为int&&,func()接收到的参数为int&&
std::cout << std::endl;
mytest2<int&&>(static_cast<int&&>(30));//此时T&& t接收到的实参为右值,T被显示指定为int&&,t的类型为int&& &&,引用折叠为int&&,func()接收到的参数为int&&
std::cout << std::endl;
return 0;
}
运行结果如图:
这里面用到了std::forward<T>()
函数,如果void mytest2(T&& t)
中不使用func(std::forward<T>(t))
,而是直接用func(t)
就达不到完美转发的要求;因为形参t
必定为左值,func(t)
只能接收到左值,所以只会调用void func(int& a)
:
#include <string>
#include <iostream>
#include <type_traits>
void func(int& a)
{
std::cout << "lvalue = " << a << std::endl;
}
void func(int&& a)
{
std::cout << "rvalue = " << a << std::endl;
}
template <typename T>
void mytest2(T&& t)
{
std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
//func(std::forward<T>(t));
func(t);
}
int main()
{
int a = 11;
mytest2(a);//此时t的类型为int&,t为左值,func()接收到的参数为int&
std::cout << std::endl;
mytest2(10);//此时t的类型为int&&,t为左值,func()接收到的参数为int&
std::cout << std::endl;
mytest2<int&&>(static_cast<int&&>(30));//此时t的类型为int&&,t为左值,func()接收到的参数为int&
std::cout << std::endl;
return 0;
}
运行结果如图:
template<class _Ty>
struct remove_reference
{ // remove reference
typedef _Ty type;
};
template<class _Ty>
struct remove_reference<_Ty&>
{ // remove reference
typedef _Ty type;
};
template<class _Ty>
struct remove_reference<_Ty&&>
{ // remove rvalue reference
typedef _Ty type;
};
std::forward()
中用到了remove_reference
,remove_reference
的作用是去除T
中的引用部分,只获取其中的类型部分。无论T
是左值还是右值,最后只获取它的类型部分。
//匹配左值
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT
{ // forward an lvalue as either an lvalue or an rvalue
return (static_cast<_Ty&&>(_Arg));
}
forward()
无法由接收的实参推导出_Ty
的左右值属性:例如当实参为int&
(或int&&
)时,会匹配到_Ty&& forward(int& _Arg)
(或_Ty&& forward(int&& _Arg)
),而remove_reference<int>::type&
、remove_reference<int&>::type&
、remove_reference<int&&>::type&
都为int&
,故无法推断_Ty
为int
、int&
还是int&&
,所以使用forward()
时需要显示指出_Ty
的类型,如forward<int>()
或forward<int&>()
等。
template <typename T>
void mytest2(T&& t)
{
//std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
//std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
//std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
//1.当T&& t接收的实参为左值时,T被推导为T&,t的类型为T&,t为左值,此时forward实例化为forward<T&>(T&)
//2.1
//mytest2(10);
//当T&& t接收的实参为右值时,T被推导为T,t的类型为T&&,t为左值,此时forward实例化为forward<T>(T&)
//2.2
//mytest2<int&&>(static_cast<int&&>(30));
//当T被显示指定为T&&时(此时形参T&& t只能接收右值实参),t的类型为T&&,t为左值,此时forward实例化为forward<T&&>(T&)
func(std::forward<T>(t));
}
//匹配右值
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{ // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
return (static_cast<_Ty&&>(_Arg));
}
//调用forward
template <typename T>
void mytest2(T&& t)
{
//std::cout << "is_reference: " << std::is_reference<T>::value << std::endl;
//std::cout << "is_lvalue_reference: " << std::is_lvalue_reference<T>::value << std::endl;
//std::cout << "is_rvalue_reference: " << std::is_rvalue_reference<T>::value << std::endl;
//1.当T&& t接收的实参为左值时,T被推导为T&,t的类型为T&,std::move(t)为右值,此时forward实例化为forward<T&>(T&&)
//此时因为T被推导为T&,为左值,static_assert断言失败,会报错
//2.1
//mytest2(10);
//当T&& t接收的实参为右值时,T被推导为T,t的类型为T&&,std::move(t)为右值,此时forward实例化为forward<T>(T&&)
//2.2
//mytest2<int&&>(static_cast<int&&>(30));
//当T被显示指定为T&&且T&& t接收的实参为右值时,t的类型为T&&,std::move(t)为右值,此时forward实例化为forward<T&&>(T&&)
func(std::forward<T>(std::move(t)));
}
移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。
template<class _Ty> inline
constexpr typename remove_reference<_Ty>::type&&
move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
//参数处根据模板推导,得出_Ty类型为_Ty、_Ty&或_Ty&&,所以_Arg可能是_Ty&或者_Ty&&。std::move的功能是:
//传递实参的是左值,推导_Arg为左值引用,仍旧static_cast转换为右值引用。
//传递实参的是右值,推导_Arg为右值引用,仍旧static_cast转换为右值引用。
//在返回处,直接返回右值引用类型即可。
//在《Effective Modern C++》中建议:对于右值引用使用std::move,对于万能引用使用std::forward。
//std::move()与std::forward()都仅仅做了类型转换而已。真正的移动操作是在移动构造函数或者移动赋值操作符中发生的。
//std::move()可以应用于左值(普通的变量int这些使用move与不使用move效果一样),但这么做要谨慎。因为一旦“移动”了左值,就表示当前的值不再需要了,如果后续使用了该值,产生的行为是未定义。
从代码中可以发现:std::move
函数将接收的实参强转为了右值引用,仅改变了其左右值属性,并不改变被转化对象本身的数据和其生命周期(这点与std::forward()
类似)。调用std::move
之后,再在移动构造函数和移动赋值运算符重载函数中实现移动语义。
一般配合std::move()
使用:
A c = std::move(A("555474", 6));
A a;
a = std::move(A("555474", 6));
A类如下:
class A
{
public:
A()
{
num = 0;
p = new char[1];
*p = '\0';
std::cout << "默认构造函数" << std::endl;
}
A(const char* str)
{
if (!str)
{
num = 0;
p = new char[1];
*p = '\0';
return;
}
const char* pp = str;
num = 0;
while (*pp != '\0')
{
pp++;
num++;
}
p = new char[num + 1];
memcpy(p, str, num);
*(p + num) = '\0';
std::cout << "构造函数1" << std::endl;
}
A(const char* str, int size)
{
if (!str || size < 1)
{
num = 0;
p = new char[1];
*p = '\0';
return;
}
num = size;
p = new char[num + 1];
memcpy(p, str, num);
*(p + num) = '\0';
std::cout << "构造函数2" << std::endl;
}
~A()
{
num = 0;
if (p)
{
delete[] p;
p = 0;
}
std::cout << "析构函数" << std::endl;
}
A(const A& a)
{
num = a.num;
p = new char[num + 1];
memcpy(p, a.p, num);
*(p + num) = '\0';
std::cout << "拷贝构造函数" << std::endl;
}
A(A&& a) noexcept
{
num = a.num;
p = a.p;
a.num = 0;
a.p = 0;
std::cout << "移动构造函数" << std::endl;
}
A& operator=(const A& a)
{
if (this == &a) return *this;
if (p) delete[] p;
num = a.num;
p = new char[num + 1];
memcpy(p, a.p, num);
*(p + num) = '\0';
std::cout << "赋值运算符重载函数" << std::endl;
return *this;
}
A& operator=(A&& a) noexcept
{
if (this == &a) return *this;
if(p) delete[] p;
num = a.num;
p = a.p;
a.num = 0;
a.p = 0;
std::cout << "移动赋值运算符重载函数" << std::endl;
return *this;
}
int num;
char* p;
};
std::move()
或static_cast<int&&>(lvalue)
等转为右值,但右值不能由static_cast<int&>(rvalue)
转为左值。std::move()
和完美转发std::forward()
。const
左值引用可以延长其绑定临时对象的生命周期。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。