原地址
作者: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||e2e1&&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.