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,非podabipod一样.即由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++好.
此外,可用同样算法确保构造器初化所有字段,结束构造器时,所有字段必须为活动状态.

posted @   zjh6  阅读(9)  评论(0编辑  收藏  举报  
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示