参考文章:

刷 Leetcode 时,时不时遇到如下 2 种遍历 STL 容器的写法:

int main()
{
vector<int> v = {1, 2, 3, 4};
for (auto &x: v)
cout<<x<<' ';
cout<<endl;
for (auto &&x: v)
cout<<x<<' ';
cout<<endl;
}

一个困扰我很久的问题是 auto &auto && 有什么区别?

左值、右值、纯右值、将亡值

首先要明确一个概念,值 (Value) 和变量 (Variable) 并不是同一个东西:

  • 值只有 类别(category) 的划分,变量只有 类型(type) 的划分。
  • 值不一定拥有 身份(identity),也不一定拥有变量名(例如 表达式中间结果 i + j + k)。

定义

左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值和将亡值。

纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。

C++( 包括 C ) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。

例子:

int i = 0; // ok, i is lvalue, 0 is rval

// 右值也可以出现在赋值表达式的左边, 但是不能作为赋值的对象,因为右值只在当前语句有效,赋值没有意义。
// 0 作为右值出现在了”=”的左边。但是赋值对象是 i 或者 j,都是左值。
(i > 0? i : j) = 233

总结:

  • 所有变量都是左值。
  • 右值都是临时的,表达式结束后不存在,立即数、表达式中间结果都是右值。

特殊情况

需要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:

class Foo
{
const char *&&right = "this is a rvalue"; // 此处字符串字面量为右值
// const char *&right = "hello world"; // error
public:
void bar()
{
right = "still rvalue"; // 此处字符串字面量为右值
}
};
int main()
{
const char *const &left = "this is an lvalue"; // 此处字符串字面量为左值
// left = "123"; // error
}

将亡值

将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念 (因此在传统 C++ 中,纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。将亡值表达式,即:

  • 返回右值引用的函数的调用表达式
  • 转换为右值引用的转换函数的调用表达式,例如 move

先看一个例子:

vector<int> foo()
{
vector<int> v = {1,2,3,4,5};
return v;
}
auto v1 = foo();

按照传统 C++ 的方式(也是我们这些 C++ 菜鸟的理解),上述代码的执行方式为:foo() 在函数内部创建并返回一个临时对象 v ,然后执行 vector<int> 的拷贝构造函数,完成 v1 的初始化,最后对 foo 内的临时对象进行销毁。

那么,在某一时刻,就存在 2 份相同的 vector 数据。如果这个对象很大,就会造成大量额外的开销。

v1 = foo() 中,v1 是一个左值,可以被继续使用,但foo() 就是一个纯右值, foo() 产生的那个返回值作为一个临时值,一 旦被 v1 复制后,将立即被销毁,无法获取、也不能修改。

而将亡值就定义了这样一种行为: 临时的值能够被识别、同时又能够被移动

在 C++11 之后,编译器为我们做了一些工作,foo() 内部的左值 v 会被进行隐式右值转换,等价于 static_cast<vector<int> &&>(v),进而此处的 v1 会将 foo 局部返回的值进行移动。也就是后面将会提到的移动语义 std::move()

个人的理解是,这种语法的引入是为了实现与 Java 中类似的对象引用系统。

左值引用与右值引用

区分左值引用与右值引用的例子

先看一段代码:

int a;
a = 2; //a是左值,2是右值
a = 3; //左值可以被更改,编译通过
2 = 3; //右值不能被更改,错误 int b = 3;
int* pb = &b; //pb是左值,&b是右值,因为它是由取址运算符返回的值
&b = 0; //错误,右值不能被更改 // lvalues:
int i = 42;
i = 43; // ok, i is an lvalue
int* p = &i; // ok, i is an lvalue
int& foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue
// rvalues:
int foobar();
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int k = j + 2; // ok, j+2 is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue
j = 42; // ok, 42 is an rvalue

那么问题来了:函数返回值是否只会是右值?当然不是。

vector<int> v(10, 0);
v[0] = 111;

显然,v[0] 会执行 [] 的符号重载函数 int& operator[](const int x) , 因此函数的返回值也是可能为左值的。

深入浅出

要拿到一个将亡值,就需要用到右值引用 T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长,只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获得一个右值临时对象,例如:

#include <iostream>
#include <string>
using namespace std;
void reference(string &str) { cout << "lvalue ref" << endl; }
void reference(string &&str) { cout << "rvalue ref" << endl; }
int main()
{
string lv1 = "string,"; // lv1 is lvalue
// string &&r1 = lv1; // 非法,右值引用不能引用左值
string &&rv1 = std::move(lv1); // 合法,move 可将左值转移为右值
cout << rv1 << endl; // string &lv2 = lv1 + lv1; // 非法,非常量引用的初始值必须为左值
const string &lv2 = lv1 + lv1; // 合法,常量左值引用能够延长临时变量的生命周期
cout << lv2 << endl; string &&rv2 = lv1 + lv2; // 合法,右值引用延长临时对象生命周期(通过 rvalue reference 引用 rval)
rv2 += "Test";
cout << rv2 << endl; reference(rv2); // 输出 "lvalue ref"
// rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。
// 也就是说,T&& Doesn’t Always Mean “Rvalue Reference”, 它既可以绑定左值,也能绑定右值
}

为什么不允许非常量引用绑定到左值?

一种解释如下(C++ 真傻逼)。

这个问题相当于解释下面一段代码:

int i = 233;
int &r0 = i; // ok
double &r1 = i; // error
const double &r3 = i; // ok

因为 double &r1 类型与 int i 不匹配,所以不行,那为什么 const double &r3 = i 是可以的?因为它实际上相当于:

const double t = (double)i;
const double &r3 = t;

在 C++ 中,所有的临时变量都是 const 类型的,所以没有 const 就不行。

移动语义

先看一段代码,熟悉一下 move 做了些什么:

#include <iostream>
#include <string>
using namespace std;
int main()
{
string a = "sinkinben";
string b = move(a);
cout << "a = \"" << a << "\"" << endl;
cout << "b = \"" << b << "\"" << endl;
}
// Output
// a = ""
// b = "sinkinben"

然后看完下面一段代码,结束这一回合。

template <class T> swap(T& a, T& b){
T tmp(a); //现有两份a的拷贝,tmp和a
a = b; //现有两份b的拷贝,a和b
b = tmp; //现有两份tmp的拷贝,b和tmp
} //试试更好的方法,不会生成额外的拷贝
template <class T> swap(T& a, T& b){
T tmp(std::move(a)); //只有一份拷贝,tmp
a = std::move(b); //只有一份拷贝,a
b = std::move(tmp); //只有一份拷贝,b
}

个人感觉,b = move(a) 这一语义操作,是把变量 b 绑定到数据 a 的内存区域上,从而避免了无意义的数据拷贝操作。

下面这一段代码可以印证我的这个观点。

#include <iostream>
class A
{
public:
int *pointer;
A() : pointer(new int(1))
{
std::cout << "构造" << pointer << std::endl;
}
A(A &a) : pointer(new int(*a.pointer))
{
std::cout << "拷贝" << pointer << std::endl;
} // 无意义的对象拷贝
A(A &&a) : pointer(a.pointer)
{
a.pointer = nullptr;
std::cout << "移动" << pointer << std::endl;
}
~A()
{
std::cout << "析构" << pointer << std::endl;
delete pointer;
}
};
// 防止编译器优化
A return_rvalue(bool test)
{
A a, b;
if (test)
return a; // 等价于 static_cast<A&&>(a);
else
return b; // 等价于 static_cast<A&&>(b);
}
int main()
{
A obj = return_rvalue(false);
std::cout << "obj:" << std::endl;
std::cout << obj.pointer << std::endl;
std::cout << *obj.pointer << std::endl;
return 0;
}
/* Output
构造0x7f8477405800
构造0x7f8477405810
移动0x7f8477405810
析构0x0
析构0x7f8477405800
obj:
0x7f8477405810
1
析构0x7f8477405810
*/

对于 queue 或者 vector,我们也可以通过 move 提高性能:

// q is a queue
auto x = std::move(q.front());
q.pop();
// v is a vertor
v.push_back(std::move(x));

如果 STL 中的元素「体积」都很大,这么做也能节省一点开销,提高性能。

完美转发

恕我直言,这个翻译是个辣鸡。英文名叫 Perfect Forwarding .

这是为了解决这样一个问题:实参被传入到函数中,当它被再传到另一个函数中,它依然是一个左值或右值。

template <class T>
void f2(T t){ cout<<"f2"<<endl; } template <class T>
void f1(T t){
cout<<"f1"<<endl;
f2(t);
//如果t是右值,我们希望传入f2也是右值;如果t是左值,我们希望传入f2也是左值
}
//在main函数里:
int a = 2;
f1(3); //传入右值
f1(a); //传入左值

在引进巴拉巴拉的这一套机制之前,即 C++11之前的情况是怎么样的呢?当我们从 f1 调用 f2 的时候,不管传入 f1 的是右值还是左值,因为 t 是一个变量名,传入 f2 的时候都变成了左值,这就会造成因为调用 T 的拷贝构造函数而生成不必要的拷贝浪费大量资源。

那么现在有一个叫 forward 的函数,就可以这样做:

template <class T>
void f2(T t){ cout<<"f2"<<endl; } template <class T>
void f1(T&& t) { //这是通用引用,而不是右值引用
cout<"f1"<<endl;
f2(std::forward<T>(t)); //std::forward<T>(t)用来把t转发为左值或右值,决定于T
}

这样,f1 调用 f2 的时候,调用的就是移动构造函数而不是拷贝构造函数,可以避免不必要的拷贝,这就叫「完美转发」。

完美转发,傻逼到家。

结语

本文开始提出的问题 auto &auto && 有什么区别?这个问题就更复杂了,涉及到 Universal Reference 这个概念,可以参考这 2 篇文章:

有空再说。

傻逼 C++ 。

左值 lvalue,右值 rvalue 和 移动语义 std::move的更多相关文章

  1. c++ 左值 和 右值

    什么是lvalue, 什么是rvalue? lvalue: 具有存储性质的对象,即lvalue对象,是指要实际占用内存空间.有内存地址的那些实体对象,例如:变量(variables).函数.函数指针等 ...

  2. C++中的左值和右值

    左值和右值的定义 在C++中,能够放到赋值操作符=左边的是左值,能够放到赋值操作符右边的是右值.有些变量既能够当左值又能够当右值.进一步来讲,左值为Lvalue,事实上L代表Location,表示在内 ...

  3. C++ 11 左值,右值,左值引用,右值引用,std::move, std::foward

    这篇文章要介绍的内容和标题一致,关于C++ 11中的这几个特性网上介绍的文章很多,看了一些之后想把几个比较关键的点总结记录一下,文章比较长.给出了很多代码示例,都是编译运行测试过的,希望能用这些帮助理 ...

  4. C++ 左值与右值 右值引用 引用折叠 => 完美转发

    左值与右值 什么是左值?什么是右值? 在C++里没有明确定义.看了几个版本,有名字的是左值,没名字的是右值.能被&取地址的是左值,不能被&取地址的是右值.而且左值与右值可以发生转换. ...

  5. c++中的左值与右值

    左值(lvalue)和右值(rvalue)是 c/c++ 中一个比较晦涩基础的概念,不少写了很久c/c++的人甚至没有听过这个名字,但这个概念到了 c++11 后却变得十分重要,它们是理解 move/ ...

  6. c++11の的左值、右值以及move,foward

    左值和右值的定义 在C++中,可以放到赋值操作符=左边的是左值,可以放到赋值操作符右边的是右值.有些变量既可以当左值又可以当右值.进一步来讲,左值为Lvalue,其实L代表Location,表示在内存 ...

  7. [C++] 左值、右值、右值引用

    一般意义上的左值(lvalue)和右值(rvalue) * lvalue 代表了对象,可通过取地址符获取地址,可赋值.L 可看做 location. * rvalue 代表了数据,不能获取内存地址,不 ...

  8. 左值与右值,左值引用与右值引用(C++11)

    右值引用是解决语义支持提出的 这篇文章要介绍的内容和标题一致,关于C++ 11中的这几个特性网上介绍的文章很多,看了一些之后想把几个比较关键的点总结记录一下,文章比较长.给出了很多代码示例,都是编译运 ...

  9. C/C++-左值、右值及引用

    目录 1.左值and右值 2.引用 3.左值引用的用途 4.std::move和std::swap C和C++中定义了引用类型(reference type),存在左值引用(lvalue refere ...

随机推荐

  1. 晶振(crystal)与谐振荡器(oscillator)

    参考: 1. https://wenku.baidu.com/view/e609af62f5335a8102d2202f.html 2. 晶体振荡器也分为无源晶振和有源晶振两种类型.无源晶振与有源晶振 ...

  2. Centos7安装MySQL8.0(RPM方式)

    人生处处皆学问,工作也是如此!过去不止一次在Linux上安装MySQL,可以说轻车熟路,但是写篇文章总结一下,发现有很多细节值得学习! 安装包选择 为什么用rpm? 在Linux系列上安装软件一般有源 ...

  3. JS-YAML -YAML 1.2 JavaScript解析器/编写器

    下载 JS-YAML -YAML 1.2 JavaScript解析器/编写器JS-YAML -YAML 1.2 JavaScript解析器/编写器 在线演示 这是YAML的实现,YAML是一种对人友好 ...

  4. BeetleX之webapi使用入门

    BeetleX是TCP通讯应用组件,在它之上可以扩展任何基于TCP的应用通讯功能.FastHttpApi是组件扩展的一个Http/Https/Websocket服务组件,它提供的功能丰富,包括功能有: ...

  5. 如何实现文章AI伪原创?

    language-ai 文章AI伪原创,文章自动生成,NLP,自然语言技术处理,DNN语言模型,词义相似度分析.全网首个AI伪原创开源应用类项目. 点击右侧about内的链接极速体验! 代码托管在gi ...

  6. LVS+keepalive

    LVS+keepalive 什么是keepalive Keepalived是Linux下一个轻量级别的高可用解决方案.高可用(High Avalilability,HA),其实两种不同的含义:广义来讲 ...

  7. GUI版本的emacs

    概要 emacs 配置 X11 配置 输入法配置 spacemacs 中的配置 fcitx 汉字显示方块的问题 总结 优势 劣势 概要 之前一直使用 terminal 版本的 emacs, 性能和显示 ...

  8. day48 Pyhton 数据库Mysql 05

    一内容回顾 insert insert into 表名 (字段名)  values (值) insert into 表名 values (有多少个字段写多少个值) insert into 表名 val ...

  9. 【原创】xenomai内核解析--实时内存管理--xnheap

    目录 一. xenomai内存池管理 1.xnheap 2. xnpagemap 3. xnbucket 4. xnheap初始化 5. 内存块分配 5.1 小内存分配流程(<= 2*PAGE_ ...

  10. beego log

    package main import ( "github.com/astaxie/beego/logs" _ "xcms/routers" _ "x ...