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++好.
 此外,可用同样算法确保构造器初化所有字段,结束构造器时,所有字段必须为活动状态.
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号