dip1040前故事
作者:deadalnix
原地址
dip1040
很好的提议.我2019年
就给W,A,A
发送邮件了,内容见下.
包含微妙而重要的点
,特别是:
1,移动后,参数无效,且未析构
2,移动精移(EMO)
,表明也转移了析构权
.
emo(精心构造的移动对象,具移动构造与移动赋值函数)
.
我当时强调了,其对二进口
影响很大.不幸的是,目的达成,但放弃
了理念.为了强调,我们对比c++
:
c++
中调用方负责析构
.即,必须用引用/指针
传递非平凡析构/移动/复制
构对象,在生成代码中引入大量间接
,更糟的是,增大分析别名难度
且阻止了优化
.
第2个缺点是构
必须有空
状态,即使被调
移动对象,他们也需要可析构
.这要求被调
置空
,而调用方
析构时检查空
状态.如析构时释放锁
的动作,则必须检查
空状态.
这是c++
的缺点,因此在提议中要显式
指出,并避免以下两点:
1,即使在abi
级按值传递非POD
对象的能力.
2,不带
空状态构造可移动
对象的能力.
上次使用
节还要改.因为不完整,如分叉:
S s;
if (...) {
//即使是
fun(s);
} else {
// ...
}
固定点分析
可包含所有基本情况
:
顺序 | 动作 |
---|---|
1 | 对所有局部变量 ,定义状态 , |
2 | 跟随控制流 更新状态 . |
3 | 合并 时,验证合并前 双分支状态 是否不同. |
4 | 修复 ,并重处理 控制流中受影响分支 . |
5 | 直到所有合并一致 即达到固定点 . |
这样,可确保现在/将来
可定义控制流
的行为.
下面是邮件内容
很难优化c++
的复制/析构
机制.我用d版共针/独针
举例.并考虑编译器内联构造/复制/析构
.
void foo(unique<T> ptr):
unique<T> t = ...;
foo(move(t));
先,独针/共针
都不是POD
,如果是POD
,则直接用指针
.在c++
中,非POD
必须有个地址
,D
也必须有个调用构造器
的地址
.此后,由于默认可移动D构
,编译器可任意发挥
了.
副作用
是c++
中,总是按引用
传非POD
了.而从不按值
.编译器在存储副本
的调用方
创建临时对象
,代码如下:
foo(T** ptrref);//引用指针
T* t = ...;
T* tmp = nullptr;
swap(t, tmp);
foo(&tmp);
if (tmp != null) destroy(tmp);
if (t != null) destroy(t);
t
总是空
,可优化掉,但tmp
不行,且每个构
必须有空
状态,对指针
没问题,但对其他构
,则是陷阱之源
.如锁
必须每次检查它引用
的互斥锁
是否为空
.你解锁
时,都要加此步骤.
假设foo
这样:
void foo(unique<T> ptr) {
global = move(ptr);
}
这里,foo
只能间接
取针值
,至少3个
周期,坏的话,未命中
缓存.且还必须
存储调用方用来检查的无效
至针
.而编译器
优化不了.
存储=>加载
转发在许多cpu
上还有性能问题
(未命中
缓存).希望,你能优化
.
如将独针=>共针
,更差.优化不了++/--
操作.导致大量移动代码
,且不注意,就有意外
的副本
了.
我建议:
1,非pod
的abi
与pod
一样.即由foo
而不是调用方
来析构
.独针
时,就可传递给寄存器
.显然,foo
中本针
与调用复制构造
时的地址不一样.
当移动
对象至函数
时,我们不能嵌套构造/析构
.但指定参数
必须从左至右
复制,从右到左
析构,则基本没问题.
在示例
中,foo
可接收寄存器
中的独针<T>
,然后,编译器知道退出函数
后,该值始终无效
,从而优化析构.
2,用以下算法
确定何时复制
:
1)遍历函数体
,跟踪左值状态:已灭,存在或已用
.参数/全局变量/可间接访问值
都是活动
的.局部变量
开始为已灭
.运行构造器/置初值
后为活动
的.
2)左值转右值
时,如返回实例/传递参数/赋值其他左值
时:
状态 | 动作 |
---|---|
活动 | 标记为已用 |
已用 | 反向跟踪 至已用 时,并插入复制 操作.复制 左值后为活动 ,并在此重启 算法. |
已灭 | 则是编译器 错误. |
需要析构
左值时:
状态 | 动作 |
---|---|
活动 | 调用析构器 , |
已用 | 不管, |
已灭 | 编译器 错误. |
上面很直接,但合并点
时,更难.如if/else
块后,对每个分支
,变量可能有不同状态
.方法:
左右状态 | 动作 |
---|---|
活动/活动 | 没问题. |
活动/已灭 | 一些分支未初化 或编译器 错误. |
活动/已用 | 跟踪已用 路径,插入复制 ,再标记左值为活动 . |
已用/已灭 | 标记为已灭 . |
赋值左值
时,其前值
移动给临时值
,赋值后析构
.临时值也有状态
,所以仅当变量
是活动
时,可析构
.
必须立即返回/赋值/按参传递/调用析构器消灭
右值.
现有返回值
优化可从此
产生.但这更通用/强大
,因为假定构
都可移动
,从而最小化复制
.
循环时,循环尾
的状态
可能影响循环头
.你必须在循环中传递
两次.回溯
也是,你可能要两次经过
代码.但,其收敛
很快,不易遇见多趟
例子,并且总是收敛
.一般在c++
编译器插入复制
处插入.
即:
shared<T> t = ...;
foo(t);
这里,移动t
的所有权
至foo
,而不复制
.
shared<T> t = ...;
foo(t);
bar(t);//稍后添加
稍后,有bar
.由编译器
插入复制
.上面两例,都不会调用析构
,因为由控制流
来调用析构器
.
这才是我们需要做的.默认移动/尽量少复制
,这样RC/ARC
的方法,都比C++
好.
此外,可用同样算法
确保构造器
初化所有字段
,结束构造器
时,所有字段
必须为活动
状态.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现