左值、右值
C++ 11对C++ 98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。
左值是指可以出现在赋值操作符(=)的左边的表达式,它们具有持久的内存地址。左值可以被取地址(&)操作符获取其内存地址,可以被修改,也可以作为参数传递给函数。
纯右值是指生成临时对象或字面量的表达式,它们通常是即将被使用的临时值。纯右值没有持久的内存地址,不能被取地址(&)操作符获取其内存地址。例如,int result = 2 + 3;
中的 2 + 3
就是一个纯右值。
将亡值是指具有资源所有权的表达式,它们可以被移动(move)而不是复制(copy)。将亡值是一种特殊的右值引用,可以延长右值的生命周期,用于实现移动语义。通过将资源所有权转移给新对象,可以避免不必要的复制操作,提高了效率。例如,std::vector<int> v1; std::vector<int> v2 = std::move(v1);
中的 std::move(v1)
就是一个将亡值。
|
左值引用、右值引用
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
右值引用和左值引用都是属于引用类型,并且都是左值。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
int &a = 2; # 左值引用绑定到右值,编译失败 |
右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:
int a; |
下表列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
std::move()
move作用是可以将一个左值转换成右值引用,从而可以调用C++11的拷贝构造函数。
std::move()的实现
std::move的实现主要依赖于static_cast<T&& >,但同时也会做一些参数推导(traits)的工作。其实现如下:
template<typename T> |
对于t为右值的情况,有如下代码:
std::move(string("dengwen")); |
首先模板类型推导确定T的类型为string,得remove_reference::type为string,故返回值和static的模板参数类型都为string &&,而move的参数就是string &&,于是不需要进行类型转换直接返回。
对于t为左值的情况,引入一条规则:当将一个左值传递给一个参数是右值引用的函数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用。有如下代码:
string str("dengwen"); |
此时明显str是一个左值,首先模板类型推导确定T的类型为string &,得remove_reference::type为string。故返回值和static的模板参数类型都为string &&,而move的参数类型为string& &&,折叠后为sting &。
所以结果就为将string &通过static_cast转为string &&。返回string &&。
引用折叠
1.所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& &&
变成 A&&
)
2.所有的其他引用类型之间的折叠都将变成左值引用。 (A& &
变成 A&
; A& &&
变成 A&
; A&& &
变成 A&
)
完美转发
考虑下面例子:
template <typename T> |
在这个例子当中,我们的期待是,我们在main当中调用relay,Test的临时对象作为一个右值传入relay,在relay当中又被转发给了func,那这时候转发给func的参数t也应当是一个右值。也就是说,我们希望:当relay的参数是右值的时候,func的参数也是右值;当relay的参数是左值的时候,func的参数也是左值。
那么现在我们来运行一下这个程序,我们会看到,结果与我们预想的似乎并不相同:
default constructor |
我们看到,在relay当中转发的时候,调用了复制构造函数,也就是说编译器认为这个参数t并不是一个右值,而是左值,因为它有一个名字。那么如果我们想要实现我们所说的,如果传进来的参数是一个左值,则将它作为左值转发给下一个函数;如果它是右值,则将其作为右值转发给下一个函数,我们应该怎么做呢?
这时,我们需要std::forward<T>()
。与std::move()相区别的是,move()会无条件的将一个参数转换成右值,而forward()则会保留参数的左右值类型。所以我们的代码应该是这样:
template <typename T> |
现在运行的结果就成为了:
default constructor |
而如果我们的调用方法变成:
int main() { |
那么输出就会变成:
default constructor |
完美地实现了我们所要的转发效果。
forward()的实现
std::forward()提供两个重载版本, 一个针对左值, 一个针对右值。
template<typename _Tp> |
根据以下实例进行分析:
template<typename T> |
在foo(i), 如果传入的是一个左值, 那么foo中T的类型将是int&, fparam类型是int& &&, 经过折叠为int&. 因此在std::forward模板函数中,推断出T的类型为int&,因此,std::remove_reference用int& 进行实例化。std::remove_reference的type成员是int。forward返回类型为int& &&, 折叠为int&。forward的参数类型__t为int&。static_cast<int & &&> 折叠为static_cast<int &>。
因此std::forward最终被实例化如下:
int &forward(int &__t){ |
可以发现,函数什么都不用做, 最终的传入forward的左值引用被保留了。
在foo(47)中, 传入的是一个右值,那么foo中T的类型将是int, fparam类型是T&&, 因此,在std::forward模板函数中推断出T的类型为int。因此, std::remove_reference用int 进行实例化。std::remove_reference的type成员是int。forward返回类型为int&&。forward的参数类型__t为int&&。static_cast<int && &&> 折叠为static_cast<int &&>
因此std::forward最终被实例化如下:
int &&forward(int &&__t){ |
可以发现,函数什么都不用做, 最终的传入forward的右值引用被保留了。
通过以上分析, 实际上无论传递左值还是右值, forward都可以完美转发, 并且函数内部什么都不用做。
函数返回值是左值还是右值
-
如果函数返回值是引用类型,则为左值。
-
如果函数返回值是值类型,则为右值。
如何判断一个值是左值还是右值
右值是能够赋值给左值,但是左值不能赋值给右值。
Reference
[1] C++ 左值和右值: https://blog.csdn.net/TABE_/article/details/122609775