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

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

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