dip1040复制移动与前向
原地址
作者:W.B,M.H.
简介
介绍移动引用,用来在当通过函数层传递,或构造/赋值对象时,不生成额外对象的机制.引入移动构造/赋值.
原理
从函数返回对象时,优化命名中值消除冗余对象.而反向传递对象至函数时,按引用传参,仅部分解决冗余问题.特别是,当对象是右值时,移动更有效.因为复制要创建+析构.同样,如果函数参数是对象的最后使用,则可移动,而不必复制.
同样,构造/赋值中右值/最后使用时,移动比复制更有效.
多函数调用右值/最后使用时,不会产生副本.
D当前无法链接至C++的右值引用参数,因为D没有右值引用概念.
初化
构造器通过复制/移动值至内存来初化变量 .相应叫复制/移动(构造器).
赋值
两步:1,析构已有值,2,复制/移动新值.合二为一,会提升效率.
参数.
考虑f函数:
构 S{...}
空 f(S s);
s构造至参数,我们需要尽量移动(效率更高),如用右值调用函数.
f(S());
应移动值.
S s;
f(s);//复制
f(s);//复制
f(s);//移动
即,移动不仅仅是右值,最后使用,也可以.
前向
引用 S 前向(中 引用 S s){中 s;}
我们希望前向参数,类似c++的完美转发.
空 f(S s);
...
S s;
f(前向(s));//复制
f(前向(s));//复制
f(前向(s));//移动(最后使用)
f(前向(S());//移动(右值)
在前向时,无移动/复制等副作用.
D没有的问题
c++仅允许转换右值为常引用.这导致c++完美转发一半的问题,但,d可转换右值为可变引用,则无此问题.
先前工作
c++
前向的问题,通过添加右值引用来解决.
rust
默认可移动,只有实现复制特征,才可复制.实际上,如何传值给函数是不变的(一般是复制内存,但llvm是随意的,你甚至可传指针),唯一区别是用作参数的变量不是移动过来的,因而可在调用函数后用它.因而,带析构器(实现drop特征)类型不能实现复制.再者,不能勾挂复制语义.无法通过假设,复制构造器来覆盖如何复制类型.移动也类似.函数不关心是否移动/复制参数.
D现有状态
1,自动引用的参数模板:这里
2,现有右值引用实现,dconf2019AA大神.
D现在提议
saoc里程碑1报告.
移动语义dip
D的右值引用和移动构造器
马丁:如何表示右值引用
描述
本设计不产生新关键字,属性或符号.
移动构造器
移动构造器是移动,而不是复制相应的第1个参数至待构造对象.移动后,该参数无效,且未析构.这样声明S构的移动构造器:
本(S s){...}
移动构造器总是不抛的,即使未声明也是如此,如果移动构造器抛了,则是非法的.
如果构无移动构造器,但内部有可移动构造字段,则定义个默认的移动构造器.无移动构造器的字段则复制位来移动.
如果有移动赋值号的构无移动构造器,则定义默认移动构造器,并按字典序移动每一字段来实现.
如,参数是右值,或左值的最后使用,则选移动构造器.
构 S{...声明精移对象...}
...
{
S s=S();//移动构造器
S u=s; //复制构造器
S w=s; //移动构造器
}//构造版本
参数和构造器都可有类型限定器.仅可在可隐式转换参数类型为被构类型时组合.
移动赋值符
移动赋值符移动,而不是复制第1参至待构造对象的构成员赋值符.移动后,在新构造对象的内容上调用析构器.移动后,参数无效,且未析构.
S构的移动赋值符声明为:
空 赋值号(S s){...}
//看起来像赋值,但却是移动赋值.
移动赋值符也是不抛的,即使未声明.
同样,如果有移赋号子字段的构未定义移赋号,则定义默认移赋号.无移动的复制位.
有移动构造器无移赋号的,定义默认移赋号,并按字典序移动每一字段来实现.
右值参/左值最后使用,选择移赋号.
构 S{...声明精移...}
...
{
S s,u,w;
s=S();//移动赋值
u=s; //复制赋值
w=s; //移动赋值
}//赋值版本
可限定参数/移赋号类型.仅在可隐式转参数类型为符号类型时允许组合.
精移对象
移动构造器/移赋号都有的构叫精移对象.函数<=>对象(返回对象,传递至参数)时按移动.而不是按非精移的复制.
重载精移
与非精移对象规则一致.
移动引用
移动引用是引用精移的参数,但未用引用来表示.
S 函数(中 S s)//按移动引用传递形参,并按值返回
{
中 s;
}
引用 S 函数(中 引用 S s)//按引用传递形参并返回
{
中 s;
}
注意,有引用的不是移动引用.不能按移动引用传递非精移对象.
按移动引用调用
构 S{...声明精移...}
...
函数(S());//S()创建一个右值,并
//传递那个右值引用到函数()
空 函数(S s)//按移动引用(而不是按值)调用
{
...
//在此析构s
}
调用方把析构右值的责任传递/移动给函数.
构 S{...声明精移...}
...
S s;
函数(s);//如传递复制到函数,则依赖实现
如,可决定是函数(s)是s的最后使用,则可通过传s的指针来移动至函数.否则,复制一份,并传新副本的指针/地址给函数.
按值返回精移
S 函数()
{
S s;
中 s;
}
与非精移对象一样,优化命名中值(无复制/移动,直接在调用栈上构建返回值s)来减少复制.如无优化,则复制.
按移动引用返回精移
S 函数(中 S s)
{
中 s;
}
精移上的中,且中型匹配参数型,表明按移动引用返回,下面是等价的非精移版:
构 T{}//T不是精移
引用 T 函数(中 引用 T t)
{
中 t;
}//好处是`省略`引用.
函数(s)不会析构s.已传输责任至调用方.
因而,可管道化函数们,因为精移仅挨个传递了指针.
S 函数(中 S s)
{
S s2;
中 s2;//错误,不能按移动引用返回局部
}
移动引用与局部冲突了.这里的中可能只能为参数中的中吧.
按引用返回精移
按引用返回精移/非精移是错的.
引用 S f(S s)
{
中 s;//错误
}
引用 S g()
{
S s;
中 s;//错误
}
因为,不能返回局部对象引用/指针.
按引用传递精移
按引用传递精移/非精移的语义是一样的.如允许:
空 函数(引用 S);
...
S s;
空 函数(s);
仍是调用者析构.
赋值引用精移至非引用精移
考虑:
空 函数(引用 S s)
{
S t;
t=s;//复制`s`,而不是`移动`,到`t`
}
显式引用的精移不是所引用对象的物主,因而,只能复制.同样,传递引用精移至非引用精移的参数时:
空 函数(S);
空 g(引用 S s)
{
函数(s);//造s副本,必须复制,引用无所有权
}
前向
回到前向函数:
引用 S 前向(中 引用 S s){中 s;}
希望,用来这样前向参数:
空 f(S s);
...
S s;
f(前向(s));
f(前向(S());
调用者保留前向参数的所有权.因而,由调用者析构,当调用f时,是复制.
但,如果可内联前向,则允许编译器检查实现,如果实现确实仅返回参数s的引用.如果是s的最后使用,则可消除复制.如同优化命名中值,是否消除复制依赖实现.
精移与垃集
定义当前语义,以便简单的压缩垃集即可用复制内存来移动对象.限制是对象字段不能有本对象指针.当前,不是问题.跟踪对象中指针的垃集不会有问题.
搞笑的是,移动构造器可执行任意代码,在收集中会干扰跟踪内存状态,可能要固定带移动构造器的对象(不可移动).其实,这是垃集的缺点.只要没了垃集,就好了.
类对象
类对象,不能有移动构造器/移动赋值符.
最后使用
左值最后使用,是最后读写左值.是对局部变量和按值传递参数分析数据流标识的.不检查全局变量/引用参数.如,对给定f:
空 函数(S s);
空 f()
{
S s;
<语句_1>;
<语句_2>;
....
函数(s);
....
<语句n>;
}
只有在函数(s)后,都未访问s,则标记此时为s的最后使用.精移左值的最后使用为移动,否则为复制.
{
S s;
函数(s);//非最后使用,复制
函数(s);//最后使用,移动
}
中语句.在中语句中返回的本地变量/函数参数,为最后使用,如果返回表达式,且多次用x,最右边用的为最后用的,这与调用参数顺序有关.
嵌套函数和λ.包含嵌套函数和访问外部局部变量的λ函数,不遵循数据流的最后使用.原因是,可从包含函数中转义嵌套函数或λ的指针.
空 滚();
空 太阳();
动 函数()
{
S a;
空 嵌套(){滚(a);}
太阳(a);//`函数`中a为最后使用,
//但转义`嵌套`指针并`嵌套`访问`a`
中&嵌套;
}
同样分析最后使用的嵌套及模块级函数的局部变量/按值传递参数.
多最后访问.如
S 福(极 标志)
{
S s;
如(标志)
中 函数(s);
异
中 滚(s);
//中 s;
//将使上个最后访问无效,变成新最后访问`s`
}
循环语句.在以下2种情况下在当/对/每一/干当体内按最后使用访问按值传递的本地变量/函数参数:1,在结束循环((中,断,至)等跳出循环外,至细节在后面))的执行路径上访问.2,变量生命期不超过迭代.
空 滚(S s);
S 福()
{
S s;
当(条件){
如(条件2)中 s;//(1)规则,最后使用->移动
异 如(条件3)函数(s);//复制
异{
S s;
滚(s); //(2)规则,移动
}
}
中 S();
}
至.标签后面的/至前面的到前面的标签处的左值,都不是最后访问,即使特定路径下不执行至.
空 福()
{
S s1;
整 a;
l1:
++a;
函数(s1); //非最后访问.
如(a!=42)
至 l1;
S s2;
整 b=滚(s2); //非最后访问,因为`太阳(s2)`;去不比左值优先的标签
//不影响限动机
如(b!=42)至 l2;
太阳(s2);
l2:
}
即,如果你的变量在标签与至 标签之间,则不算最后使用.
静态条件,编译时条件,不影响决定最后使用机制.求值完静态条件后,运行dfa.
空 福(T)()
{
S s;
函数(s);//不用S实例化foo,
//则是最后访问s.
静 如(是(T==S))
滚(s);
异
函数(a);
}
&&||式.e1||e2或e1&&e2规则:
条件 | 最后使用 |
|---|---|
e1访问x,e2不 | 则e1为最后. |
e2访问x,e1不 | 则e2为最后. |
e2,e1都访问x | 则e2为最后. |
部分移动,聚集类型(构/类)变量可含精移字段.
空 函数(T t);
空 滚(S s);
构 S
{
T t; //T是精移
}
{
S s;
函数(s.t);
滚(s);
}
尽管函数最后访问s.t,但后面用了s.这儿,必须复制s.t.然后,如果函数为最后访问s,则可以移动t.即,仅当外包对象为最后访问时,可移动其内部的精移字段.
指针,当取x变量地址时,x失去了最后使用的优化机会.
{
S s;
整*p=&s.i;
函数(s);//非s的最后使用
*p=3; //因为仍在写s
}
{
...
S s;
S*ps=&s;
函数(s); //非最后使用
整 j=ps.i;//因为仍在读s
}
{
...
S s;
S*ps=&s;
函数(s); //ps仍指向s,所以不是最后使用,即使从未用ps
}
构 T{整 j;S s;~本();}
{
T t;
函数(t.s);//非最后使用,因为T的析构器
}
产生精移的右值表达式总是最后使用,因而用移动.
空 函数(S);
...
函数(S());//S()是右值,所以总是移动
析构.移动精移,也移动了析构责任.
S 测试(S s)
{
中 函数(s);//现在该是由函数析构s
}
合并路径之一可能移动左值的控制路径.
{
S s;
如(条件)
函数(s);//复制还是移动?
s.__析构器();//但如果移动s呢?
}
实现可用复制,可用带检查析构器调用标志的移动.
{
S s;
极 标志=真;
如(条件)
{
函数(s);//复制还是移动?
标志=假;
}
标志&&s.__析构器();
}
如果函数可能抛异常,则在调用函数前,必须置标记为假.
移动后赋值
代码片:
S s,t;
函数(s);//A
s=t; //B
两种编译方式:
1,A:复制s,B:赋值s.
2,A:移动s,B:构造s,而不是赋值.
由实现决定,1快速构建,2优化快.
原因是:非默认赋值将析构目标原内容,即它必须是活跃的.但移动操作使内容未定义.移动/构造版在运行时比复制/赋值版更有效.
示例:交换函数
如S为精移,则交换函数为:
空 交换(引用 S s,引用 S t)
{
S 临=s;
s=t;
t=临;
}
因为没用右值,用移动语义依赖实现决定每次读是最后使用.
低效
复制后移动
构 S{...声明精移...}
构 T{S s;}
空 函数(S u)
{
T t;
t.s=u;//移动
}
S g()
{
S s;
函数(s);//复制
中 s;
}
注意,先复制s,再移动u.这不如仅复制有效.如有问题,用常规引用.
构 S{...声明 精移...}
构 T{S s;}
空 函数(引用 S u)
{
T t;
t.s=u;//复制
}
S g()
{
S s;
函数(s);//按指针传递
中 s;
}
可提供两个重载:
空 函数(S);
空 函数(引用 S);
重载规则是:对左值用引用版,右值用非引用版.
对接C++
右值引用
移动引用对应c++的右值引用.即,为对接c++的右值引用,d端必须是精移对象.
//D
构 S{...声明精移...}
空 函数(S);
//C++:
构 S{...};
空 函数(S&&);
移动后对象状态
d移动对象后,留下存储区域为未定义状态.不会调用其析构器.c++移动对象时,期望存储区域为有效且可析构状态.因此,同c++对接对象,其移动构造与移赋号应使移动后的源对象可析构.最实用方法是置为.初化状态.
值参数
尽管c++的最佳实践可能是用右值引用而不是用值.但仍有大量遗留代码用值传参.为对接c++值参数,对外(c++)函数必须有强制用值语义方法.
应用在精移上的@值存储类将使它为值参数.对非精移类型参数,为方便通用代码,将忽略它.允许@值同@引用/@出存储类一起用.
感谢
Razvan Nitu, Andrei Alexandrescu, Sahmi Soulaimane, Martin Kinkelin, Manu Evans, Atila Neves, Mike Parker, Ali Cehreli.
浙公网安备 33010602011771号