惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 最新话题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Forbes - Security
Forbes - Security
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
W
WeLiveSecurity
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
Engineering at Meta
Engineering at Meta
PCI Perspectives
PCI Perspectives
Martin Fowler
Martin Fowler
T
The Exploit Database - CXSecurity.com
F
Full Disclosure
WordPress大学
WordPress大学
S
Security Affairs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
SegmentFault 最新的问题
P
Privacy International News Feed
IT之家
IT之家
M
MIT News - Artificial intelligence
G
GRAHAM CLULEY
Hacker News: Ask HN
Hacker News: Ask HN
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Check Point Blog
美团技术团队
Security Latest
Security Latest
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
MyScale Blog
MyScale Blog
H
Help Net Security
宝玉的分享
宝玉的分享
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
The Cloudflare Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AI
AI
I
InfoQ
N
News | PayPal Newsroom
TaoSecurity Blog
TaoSecurity Blog

iTimothy

那些让人笑出声的Meme排序算法 再谈费曼学习法 第一性原理读书笔记 我的2025 AWS Certified Developer Associate 认证备考指北 我的 AWS Certified Machine Learning – Specialty 认证之旅 2024年的年终总结:转变与沉淀 迟来的2023年终总结 The Heap Sort Algorithm Explained 告别2022 客制化键盘初体验 RAMA WORKS KARA 聊聊被动收入与躺赚 新加坡驾照转换指北 利用GitHub Actions实现版本自动构建与发布流程 逝去的2021 使用Caddy2托管静态博客 美股期权投资策略学习笔记--期权的基本概念 我的2020 CKAD认证备考经验分享 Emacs启动加速篇
Modern C++ 学习笔记 -- 左值与右值
Timothy · 2023-09-07 · via iTimothy

左值(lvalues)与右值(rvalues)的概念

左值和右值是Modern C++中引入的新概念。简而言之:

  • 左值位于等号左边,我们可以对左值进行取地址操作。
  • 右值位于等号右边,本质上是一个数值,即 literal constant,我们没法对它进行取地址操作。
1
int x = 999; 

我们,可以把左值大致想象为一个容器,而容器中存放的就是右值,如果没有这个容器,其中的右值将会失效。对于以下的程序,我们在编译的时候将会得到类似的错误:

1
error: lvalue required as left operand of assignment
1
2
int x;
123 = x;

很显然,等号左边需要的是一个左值,而123作为一个 literal constant 类型,是没有办法充当左值的。同样,我们也没法对一个右值进行取地址的操作:

1
2
int *x;
x = &123;

编译器报错:

1
error: lvalue required as unary '&' operand

左值到右值的隐式转换

左值在很多情况下有可能被转换为右值,比如在C++中的 - 运算符,它将两个右值作为参数,并将计算结果作为右值返回。

1
2
3
int x = 10;
int y = 5;
int z = x - y;

在上面的程序片段中,我们明显看到x, y本身是左值,但是却以右值的身份参与了减法运算。这是如何做到的呢?答案是编译器对左值做了隐式的转换,将x和y转换成为了右值。C++中其他的乘法,除法和加法运算也是同样如此。

如果左值能被转换成右值,那么右值本身能被转换成左值吗?答案是 否定 的.

左值引用与右值引用

C++中引入引用的概念,是为了在程序中方便的通过引用修改原变量的值,并且,在调用方法传参的过程中,传递引用可以避免拷贝。在通常情况下,左值引用只能指向左值,而右值引用只能指向右值。听起来比较废话,但是也有特殊的情况。

左值引用

1
2
3
int x = 10;
int& ref_x = x;
ref_x++;

在上面的示例程序中,我们定义了一个左值x,然后赋值10。随后定义了一个引用,指向x。因此,ref_x成为x的引用,它就叫做左值引用。通过操作ref_x,我们就可以改变x的值。

如果我们把上面的程序简化为:

1
int& ref_x = 10;

在编译的时候,我们会得到类似的错误:

1
cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

显然,左值引用只能指向一个左值,而不能指向一个右值。不错从错误信息中,我们方法可以得出另外一种写法:

1
const int& ref_x = 10; 

根据编译器的规则,我们被允许通过定义一个const类型的左值来指向右值。不过既然这个左值被定义成了const,没有办法修改指向的值。

右值引用

C++中的右值引用以&&表示。通过右值引用,可以修改其指向的右值。

1
2
int&& ref_x = 10; 
ref_x--;

如果我们尝试将右值引用指向一个左值:

1
2
int x = 10;
int&& ref_x = x;

编译器也会抛出类似的错误,告诉我们不能把一个右值引用指向左值。

1
error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'

左右值引用的本质

通过一个简单的示例程序,我们就能知道左值引用和有值引用的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void increase(int&& input) {
input++;
}

int main() {
int x = 10;

int& ref_a = &x;
int&& ref_b = std::move(x);

increase(x);
increase(ref_a);
increase(ref_b);

increase(std::move(a));
increase(std::move(ref_a));
increase(std::move(ref_b));

increase(7);

return 0;
}

从上面的代码示例中,我们可以看出右值引用 ref_b 本身也是一个左值,在调用 increase 的时候,需要通过 std::move 转换为右值,编译器才不会报错。

通过以上的例子,我们可以总结出如下的规律:

  • 左右值引用的引入,都是为了避免拷贝。
  • 左值引用通常指向左值,通过添加 const 关键字约束也能指向右值,不过无法对右值进行修改。
  • 右值引用本质上也是一个左值,右值引用通常情况下指向右值,不过通过 std::move 等形式也可以指向左值。

右值引用与移动语义

在前面的例子中,我们已经涉及到了 std::move 这样的操作。右值引用配合 std::move 通常能实现移动语义,从而实现避免拷贝,提升程序性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>

int main() {
std::vector<std::string> list;

std::string str_a = "Hello";
std::string str_b = "World";

list.push_back(str_a);
list.push_back(std::move(str_b));

std::cout << "str_a: " << str_a << std::endl;
std::cout << "str_b: " << str_b << std::endl;
std::cout << "list[0]: " << list[0] << std::endl;
std::cout << "list[1]: " << list[1] << std::endl;
return 0;
}

如果运行上面的示例程序,我们会得到这样的程序输出:

1
2
3
4
str_a: Hello
str_b:
list[0]: Hello
list[1]: World

很明显,在str_a被添加到vector的时候,并没有涉及到移动语义,所以str_a的值被拷贝到了vector中。而在把str_b添加到vector的过程中,由于用到了std::move,所以str_b的值被移动到了vector中。之后再输出vector的值的时候,可以看到其中已经包含了str_astr_b的值。但是str_b本身的值已经被偷走了。

需要注意的是,std::move本身的名字比较有迷惑性,其实它在这里的工作只是把str_b从左值转换成右值,而不会做实际上移动资源的操作。如果我们把添加str_b的代码替换成:

1
list.push_back(static_cast<std::string&&>(str_b));

会达到一样的效果。而真正的秘密在于 std::vector 提供的两种不同的重载方法:

1
2
void push_back( const T& value );
void push_back( T&& value );

第一个重载方法接受的是左值引用,当传入 str_a 的时候,由于 const 关键字的限制,它的值会被拷贝一份,并放入到vector中,而 str_a 本身的值并不受影响。而第二个重载方法接受的是一个右值引用,push_back方法会把其值放入vector中,并转移 str_b 对字符串值 World 的所有权。这样,当我们再输出它的值的时候,发现已经为空了。

完美转发(std::forward)

完美转发(Perfect Forwarding),转发的意义在于当一个方法把其参数传递给另外一个方法的时候,不光转发参数本身,还包括其属性(左值引用保持左值引用,右值引用保持右值引用)。

std::forward 实际上也是做类型的转换,不同的是 std::move 只把左值转换为右值,std::forward 能转换为左值或右值。

std::forward<T>(arg) 中,如果类型 T 是左值,参数 arg 将被转换为左值,否则 arg 将被转换为右值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <utility>

void target_func(int& arg) { std::cout << "lvalue reference" << std::endl; }

void target_func(int&& arg) { std::cout << "rvalue reference" << std::endl; }

template <typename T> void forward(T&& arg) {
target_func(std::forward<T>(arg));
}

int main() {
forward(5);
int x = 5;
forward(x);
return 0;
}

在以上的示例程序中,forward 使用一个Universal Reference类型接受一个参数,并且通过 std::forward 讲参数转发给 target_func。由于此方法有两个重载,分别接受左值引用和右值引用。在我们分别传入右值 5 和左值 x 的时候,我们发现 forward 这个方法都能准确无误的把参数转发给对应的重载方法。因此,程序的输出分别是:

1
2
rvalue reference
lvalue reference

参考文章

  1. Understanding the meaning of lvalues and rvalues in C++
  2. Cpp Reference
  3. Perfect Forwarding