C++|值类别(左值、右值)、移动语义、省略拷贝(返回值优化)
yuyutoo 2024-12-14 15:53 3 浏览 0 评论
每个 C++ 表达式都有两个属性:类型 (type) 和值类别 (value category)。
type 和 category 都可以翻译为“类型”或“类别”,但为了区分两者,下文中统一将 type 翻译为“类型”,category 翻译为“类别”。
1 从 CPL 语言的定义说起
左值与右值的概念最早出现在 C 语言的祖先语言:CPL。
在 CPL 的定义中,lvalue 意为 left-hand side value,即能够出现在赋值运算符(等号)左侧的值,右值的定义亦然。
2 C 和 C++11 以前
C 语言沿用了相似的分类方法,但左右值的判断标准已经与赋值运算符无关。
const int a = 0; // a这个id-expr是左值表达式,但a不能被放在赋值运算符左侧
"Luogu" // 字符串常量是左值表达式,但是常量表达式显然不能放在运算符左侧
int arr[3]; // arr作为id-expr时是左值表达式,但是不能被放在赋值运算符左侧
在新的定义中,lvalue 意为 locate value,即能进行取地址运算 (&) 的值。
可以这么理解:左值是有内存地址的对象,而右值只是一个中间计算结果(虽然编译器往往需要在内存中分配地址来储存这个值,但这个内存地址是无法被程序员感知的,所以可以认为它不存在)。中间计算结果就意味着这个值马上就没用了,以后不会再访问它。
比如在 int a = 0; 这段代码中,a 就是一个左值,而 0 是一个右值。
++i 和 i++ 是典型的左值和右值。++i 的实现是直接给 i 变量加一,然后返回 i 本身。因为 i 是内存中的变量,因此可以是左值。实际上前自增的函数签名是 T& T::operator++();。而 i++ 则不一样,它的实现是用临时变量存下 i,然后再对 i 加一,返回的是临时变量,因此是右值。后自增的函数签名是 T T::operator++(int);。
int n1 = 1;
int n2 = ++n1;
int n3 = ++ ++n1; // 因为是左值,所以可以继续操作
int n4 = n1++;
// int n5 = n1++ ++; // 错误,无法操作右值
// int n6 = n1 + ++n1; // 未定义行为
int&& n7 = n1++; // 利用右值引用延长生命期
int n8 = n7++; // n8 = 1
常见的关于左右值的误解
以下几种类型是经常被误认为右值的左值:
- 字符串字面量:由于 C++ 兼容 C 风格的字符串,需要能对一个字符串字面量取地址(即头指针)来传参。但是其他的字面量,包括自定义字面量,都是右值。
- 数组:数组名就是数组首个元素的指针这种说法似乎误导了很多人,但这个说法显然是错误的,对数组进行取地址是可以编译的。数组名可以隐式地退化成首个元素的指针,这才是右值。
3 C++11 开始
从 C++11 开始,为了配合移动语义,值的类别就不是左值右值这么简单了。
考虑一个简单的场景:
std::vector<int> src{...};
std::vector<int> dst;
dst = src;
我们知道第三行的赋值运算复杂度是正比于 src 的长度的,复制的开销很大。但有些情况下,比如 src 在以后的代码中不会再使用,那么我们完全可以把 src 所持有的内存“转移”到 dst 上,这就是移动语义干的事情。
我们姑且不管移动是怎么实现的,先来考虑一下我们如何标记 src 是可以移动的。显然不管能否移动,这个表达式的类型都是 vector 不变,所以只能对值类别下手。不可移动的 src 是左值,如果要在原有的体系下标记可以移动的 src,我们只能把它标记为右值。但标记为右值又是不合理的,因为这个 src 实际上拥有自己的内存地址,与其他右值有根本上的不同。所以 C++11 引入了 亡值 (xvalue) 这一值类别来标记这一种表达式。
于是我们现在有了三种类别:左值 (lvalue)、纯右值 (prvalue)(纯右值就是原先的右值)、亡值 (xvalue)。
然后我们发现亡值同时具有一些左值和纯右值的性质,比如它可以像左值一样取地址,又像右值一样不会再被访问。
所以又有了两种组合类别:泛左值 (glvalue)(左值和亡值)、右值 (rvalue)(纯右值和亡值)。
有一个初步的感性理解后,来看一下标准委员会对它们的定义:
- A glvalue(generalized lvalue) is an expression whose evaluation determines the identity of an object, bit-field, or function.
- A prvalue(pure rvalue) is an expression whose evaluation initializes an object or a bit-field, or computes the value of an operand of an operator, as specified by the context in which it appears, or an expression that has type cv void.
- An xvalue(eXpiring value) is a glvalue that denotes an object or bit-field whose resources can be reused(usually because it is near the end of its lifetime)。
- An lvalue is a glvalue that is not an xvalue.
- An rvalue is a prvalue or an xvalue.
其中关键的两个概念:
- 是否拥有身份 (identity):可以确定表达式是否与另一表达式指代同一实体,例如比较它们所标识的对象或函数的(直接或间接获得的)地址。
- 是否可以被移动 (resources can be reused):对象的资源可以移动到别的对象中。
这 5 种类型无非就是根据上面两种属性的是与否区分的,所以用下面的这张表格可以帮助理解:
拥有身份(glvalue) | 不拥有身份 | |
可移动(rvalue) | xvalue | prvalue |
不可移动 | lvalue | 不存在 |
注意不拥有身份就意味着这个对象以后无法被访问,这样的对象显然是可以被移动的,所以不存在不拥有身份不可移动的值。
4 移动语义和std::move(C++11)
在 C++11 之后,C++ 利用右值引用新增了移动语义的支持,用来避免对象在堆空间的复制(但是无法避免栈空间复制),STL 容器对该特性有完整支持。具体特性有移动构造函数、移动赋值和具有移动能力的函数(参数里含有右值引用)。 另外,std::move 函数可以用来产生右值引用,需要包含 <utility> 头文件。
注意:一个对象被移动后不应对其进行任何操作,无论是修改还是访问。被移动的对象处于有效但未指定的状态,具体内容依赖于 STL 的实现。如果需要访问(即指定一种状态),可以使用该对象的 swap 成员函数或者偏特化的 std::swap 交换两个对象(同样可以避免堆空间的复制)。
// 移动构造函数
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> v2(std::move(v)); // 移动v到v2, 不发生拷贝
// 移动赋值函数
std::vector<int> v3;
v3 = std::move(v2);
// 有移动能力的函数
std::string s = "def";
std::vector<std::string> numbers;
numbers.push_back(std::move(s));
当右值引用指向的空间在进入函数前已经分配时,右值引用可以避免返回值拷贝。
struct Beta {
Beta_ab ab;
Beta_ab const& getAB() const& { return ab; }
Beta_ab&& getAB() && { return std::move(ab); }
};
Beta_ab ab = Beta().getAB(); // 这里是移动语义,而非拷贝
5 C++17 带来的新变化
从拷贝到移动提升了不少速度,那么我们是否能够优化的更彻底一点,把移动的开销都省去呢?
考虑这样的代码:
std::vector<int> make_vector(...) {
std::vector<int> result;
// ...
return result;
}
std::vector<int> a = make_vector(...);
make_vector 函数根据一输入生成一个 vector。这个 vector 一开始在 make_vector 的栈上被构造,随后又被移动到调用者的栈上,需要一次移动操作,这显然很浪费,能不能省略这次移动?
答案是肯定的,这就是 RVO(Return Value Optimization) 优化,即省略拷贝。通常的方法是编译器让 make_vector 返回的对象直接在调用者的栈上构造,然后 make_vector 在上面进行修改。这相当于这样的代码:
void make_vector(std::vector<int>& result, ...) {
// ... (对 result 进行操作)
}
std::vecctor<int> a;
make_vector(a, ...);
在 C++17 以前,尽管标准未做出规定,但主流编译器都实现了这种优化。在 C++17 以后,这种优化成为标准的硬性规定。
回到和移动语义刚被提出时的问题,如何确定一个移动赋值是可以省略的?再引入一种新的值类别?
不,C++11 的值类别已经够复杂了。我们意识到在 C++11 的标准下,亡值和纯右值都是可以移动的,那么就可以在这两种类别上做文章。
C++17 以后,纯右值不再能移动,但可以隐式地转变为亡值。对于纯右值用于初始化的情况下,可以省略拷贝,而其他不能省略的情况下,隐式转换为亡值进行移动。
所以在 C++17 之后的值类别,被更为整齐的划分为泛左值与纯右值两大块,右值存在的意义被削弱。这样的改变某种程度上简化了整个值类别体系。
C++17开始纯右值不可被移动,并且引入了强制复制消除的要求(mandatory copy elision),达到了史无前例的值类别最复杂的阶段,另外,void表达式开始指代一个无结果的对象,也成为了不可被移动且不具有同一性的纯右值。
ref
https://oi-wiki.org/lang/reference/
https://www.luogu.com.cn/blog/SuperConstructor/qian-tan-zhi-lei-bie-ji-ji-li-shi
-End-
相关推荐
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
-
IT之家4月3日消息,科技媒体WindowsLatest昨日(4月2日)发布博文,报道称Windows10、Windows11新版Copilot应用已摘掉Beta帽...
- WinForm 双屏幕应用开发:原理、实现与优化
-
在当今的软件开发领域,多屏幕显示技术的应用越来越广泛。对于WinForm应用程序来说,能够支持双屏幕显示不仅可以提升用户体验,还能满足一些特定场景下的业务需求,比如在演示、监控或者多任务处理等场景...
- OpenJDK 8 安装(openjdk 8 windows)
-
通常OpenJDK8和11都能互相编译和通用。我们建议使用11,但是如果你使用JDK8的话也是没有问题的。建议配置使用OpenJDK,不建议使用OracleJDK,主要是因为版...
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
-
一、前言在如今远程办公和内网穿透需求日益增长的背景下,搭建一套安全、稳定、高效的VPN系统显得尤为重要。OpenConnectServer(ocserv)是一个开源、高性能的VPN服务端软件...
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
-
虽然现在新版本的Edge浏览器已经推出,但是毕竟还处于测试的状态中。而Win10系统里面自带的老版Edge浏览器,却越来越不被人重视。其实我们只需要根据实际情况对老版本的Edge浏览器进行一些简单的设...
- 微软开源博客工具Open Live Writer更新:多项Bug修复
-
OpenLiveWriter前身是WindowsLiveWriter,是微软WindowsLive系列软件之一,曾经是博主们非常喜爱的一款所见即所得博文编辑工具,支持离线保存,还支持图像编辑...
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
-
在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。...
- Python open函数详解(python open函数源码)
-
演示环境,操作系统:Win1021H2(64bit);Python解释器:3.8.10。open是Python的一个内置函数,一般用于本地文件的读写操作。用法如下。my_file=open(fi...
- 世界上最好用的Linux发行版之一,OpenSUSE安装及简单体验
-
背景之前无意在论坛里看到openSUSE的Linux发行版,被称为世界上最好用的Linux发行版之一(阔怕),一直想体验一下,于是这期做一个安装和简单体验教程吧。...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
推荐7个模板代码和其他游戏源码下载的网址
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- 微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型
- WinForm 双屏幕应用开发:原理、实现与优化
- 推荐一个使用 C# 开发的 Windows10 磁贴美化小工具
- OpenJDK 8 安装(openjdk 8 windows)
- 基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)
- 巧妙设置让Edge浏览器更好用(edge怎么设置好用)
- WPF做一个漂亮的登录界面(wpf页面设计)
- 微软开源博客工具Open Live Writer更新:多项Bug修复
- 基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品
- C#开源免费的Windows右键菜单管理工具
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)