C++左值与右值

一、左值与右值的定义:

  • 一个 lvalue 是通常可以放在等号左边的表达式,左值
  • 一个 rvalue 是通常只能放在等号右边的表达式,右值
  • 一个 glvalue 是 generalized lvalue,广义左值
  • 一个 xvalue 是 expiring lvalue,将亡值
  • 一个 prvalue 是 pure rvalue,纯右值

左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:变量、函数或数据成员的名字返回左值引用的表达式,如 ++x、x = 1、cout << ’ '字符串字面量如 "hello world"在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。

反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:返回非引用类型的表达式,如 x++、x + 1、make_shared(42)除字符串字面量之外的字面量,如 42、true

函数调用时:
左值: 可以绑定到左值引用的参数,如 T&
右值: 在 C++11 之前,右值可以绑定到常左值引用,如 const T&

二、C++11之后:右值引用

左值引用: T&
右值引用: T&&

右值引用作用:

右值引用主要作用是解决大对象在作为函数返回值返回时的深度拷贝问题,以及大对象之间的快速复制。

void process_value(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
process_value(a);
process_value(1);
}
运行结果 :
LValue processed: 0
RValue processed: 1

三、引用塔缩(引用折叠)

对于一个实际的类型 T,它的左值引用是 T&,右值引用是 T&&。
那么:是不是看到 T&,就一定是个左值引用?是不是看到 T&&,就一定是个右值引用?

对于前者的回答是“是”,对于后者的回答为“否”

  • 对于 template foo(T&&) 这样的代码,如果传递过去的参数是左值,T 的推导结果是左值引用;
  • 如果传递过去的参数是右值,T 的推导结果是参数的类型本身。
  • 如果 T 是左值引用,那 T&& 的结果仍然是左值引用——即 type& && 坍缩成了 type&。如果 T 是一个实际类型,那 T&& 的结果自然就是一个右值引用。

引用折叠规则:

  • X& &—>X&
  • X& &&—>X&
  • X&& &—>X&
  • X&& &&—>X&&
void foo(const shape&)
{
  puts("foo(const shape&)");
}
void foo(shape&&)
{
  puts("foo(shape&&)");
}
void bar(const shape& s)
{
  puts("bar(const shape&)");
  foo(s);
}
void bar(shape&& s)
{
  puts("bar(shape&&)");
  foo(s);
}
int main()
{
  bar(circle())
}
输出为:
bar(shape&&)
foo(const shape&)

四、std::move: 将左值引用转换成右值引用,用于移动语义

std::move基本等同于一个类型转换:
T& lvalue;
T&& rvalue = static_cast(lvalue);

在这里插入图片描述 move定义可以看出,move并没有”移动“什么内容,只是将传入的值转换为右值,此外没有其他动作,std::move+移动构造函数或者移动赋值运算符,才能充分起到减少不必要拷贝的意义。

void ProcessValue(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void ProcessValue(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
ProcessValue(a);
ProcessValue(std::move(a));

使用场景:

  1. 已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用
void ProcessValue(int& i) {
std::cout << "LValue processed: " << i << std::endl;
}
void ProcessValue(int&& i) {
std::cout << "RValue processed: " << i << std::endl;
}
int main() {
int a = 0;
ProcessValue(a);
ProcessValue(std::move(a));
}
  1. 提高 swap 函数的的性能上非常有帮助
template <class T> swap(T& a, T& b)
   {
       T tmp(a);   // copy a to tmp
       a = b;      // copy b to a
       b = tmp;    // copy tmp to b
}
有了 std::move,swap 函数的定义变为 :
  template <class T> swap(T& a, T& b)
   {
       T tmp(std::move(a)); // move a to tmp
       a = std::move(b);    // move b to a
       b = std::move(tmp);  // move tmp to b
}
  • 移动构造函数
    C++左值与右值_第1张图片
    通过移动构造,b指向a的资源,a不再拥有资源,这里的资源,可以是动态申请的内存,网络链接,打开的文件,也可以是本例中的string。这时候访问a的行为时未定义的,比如,如果资源是动态内存,a被移动之后,再次访问a的资源,根据移动构造函数的定义,可能是空指针,如果是资源上文的string,移动之后,a的资源为空字符串(string被移动之后,为空字符串)。

std::move()优点:

  • std::move语句可以将左值变为右值而避免拷贝构造。
  • std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。

- 五、std::forward: 完美转发

完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

Foo(string&& member): member{std::forward<string>(member)} {}

std::move()和std::forward()对比

  • std::move执行到右值的无条件转换。就其本身而言,它没有move任何东西。
  • std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
  • std::move和std::forward只不过就是执行类型转换的两个函数;std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。
  • std::forward()不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;

你可能感兴趣的:(C++,c++)