d结构的移动语义

勾挂D的结构移动语义

概述

当前语言定义禁止结构类型维护到其实例外部/内部引用,因为D可能选择通过简单的位复制操作来移走结构实例.

此DIP的目的是保留此函数,并也允许到实例的内部/外部引用.这是通过允许该结构定义类似后复制的回调(叫后移动操作)来实现的,在移动后调用它,从而允许该结构更新移动搞失效的引用.

参考

#17448问题:由于缺乏这种支持而引起的问题,以及有关为什么需要这种支持的讨论.
C++解决同一问题的方法

理由

允许D编译器移动(而不是破坏)已到达域尾栈分配的结构对象.尽管这很有用,但表明编程模式更难.

通常表述该限制为"D结构可能不含指向自身的指针".尽管该限制是正确的,但它不是唯一.例如,D结构也可能不在构造/析构函数向跟踪系统中的所有实例的全局注册表注册自己,即通过链式列表.这也严重限制了从结构外部存储引用结构实例的闭包的能力.

尽管并非所有这些情况都可以通过此DIP轻松解决,但如果没有它,程序员将没法解决该问题,即使她很幸运的提前发现内存崩溃问题.

描述

大概

DIP建议修改:

DRuntime中加一个*__后移动的函数
用户可在构中定义后移动操作成员函数.如果定义,该函数必须遵循良好定义的接口.
决定移动结构实例时,编译器必须在移动实例和释放包含旧实例内存间调用
__后移动*. __后移动必须接收移动前后实例的引用.

__后移动的实现

应按与以下代码兼容的方式定义*__后移动*:

空 __后移动(S)(ref S 新位置,ref S 旧位置)不抛
如((S ==)){
     每一(成员名; __特征(allMembers,S)){
         静态 如((typeof运算(__特征(getMember,S ,成员名))==)){
             插件(" __后移动(新位置. "  ?成员名? "旧位置. " ?成员名?");");
        }
    }
    静态 如(__特征(hasMember,S,"后移动操作")){
        新位置.后移动操作(旧位置);
    }
}

注意,S也可为shared,immutable或const.

后移动操作

后移动操作(如果已定义),则必须是在复制外部/内部引用后更新它们的不抛函数.实施者应使其@nogc与@safe或@trusted.

实现者可定义后移动操作常和/或不变版本.如果是这样,实现代码可安全地在对象目标位置修改数据,因为无指向对象的指针.需要转换此类修改.

修改按指针引用的结构中存储的外部数据在移动中的源地址的数据是否安全(如侵入式链表),在很大程度上取决于这些指针的精确语义细节.后移动操作的用户文档必须说明什么是安全及不安全的.

实现者也可选择不定义常/不变版本的后移动操作.如果编译器尝试移动此类结构的实例,将导致编译时错误.

后移动操作的实现文档还必须强调,虽然允许在后移动操作源位置处操作内存,但函数返回后,将无害的释放内存.应鼓励实现者按常 引用定义后移动操作的源参数,以获得编译器防止意外操作的保护.这不会影响实施者访问数据,因为她已在目标位置拥有副本了.

编译器移动时发出的代码

移动结构的实例时,如果提供新旧实例的地址,编译器必须调用*__后移动*.

后移动操作 装饰注意事项

后移动操作应是@nogc和@safe或@trusted.如没有,则编译从内部上下文是@nogc或@safe的结构实例移动的代码,会导致编译错误.后移动操作,如实施,必须为不抛.

可通过按@不抛 @nogc @safe修饰*__后移动自身来强制使用这些属性,从而避免按其他任何方式定义后移动操作.不建议这样做,比如用户可能从不用@safe.
强迫她在电脑上使用后移动操作是没有意义的.
由于模板函数的属性推导,如果后移动操作所有实例成员都是,如@nogc,d会自动对该结构按@nogc定义
__后移动*

提议确实要求在后移动操作不抛,抛出就意味着程序流的变化.

示例

为了方便讨论,以下是需要后移动操作来保持正确性的具体示例.

内部引用

考虑跟踪数字的结构.这可是局部(每个结构)编号或全局编号.一种可能实现是:

struct Tracker {
    static uint globalCounter;
    uint localCounter;
    bool isLocal;

    @禁用 this(this);

    this(bool local){
        isLocal =local;
        localCounter = 0 ;
    }

    空 增量(){(isLocal)
            localCounter++;
        否则 
            globalCounter++;
    }
}

使用如子句来确定更新谁会使性能很差.除非成功预测分支,否则在现代CPU上分支是昂贵操作.
如果该结构的实例在全局和本地更新间均匀分布,则分支预测率为50%(白预测了),此实现性能将非常差.

让isLocal是个模板参数能解决分支预测问题,但只能在编译时就能决定本地与全局时才行.

性能更好的方法是使用指针:

struct Tracker{
    static uint globalCounter;
    uint localCounter;
    uint *计数器;

    @禁用 this(this);

    这个(bool local){
        localCounter = 0 ;(本地)
            计数器=&localCounter;
        其他
            计数器=&globalCounter;
    }

    空 增量(){
        (*计数器)++ ;
    }

    void 后移动操作(const ref Tracker 旧位置){(计数器 是 &旧位置.localCounter)
            计数器=&localCounter;
    }
}

性能考量

对于未定义及成员未定义后移动操作的结构,将导致对其成员实例递归调用空函数实现.编译器应能够通过内联消除这一系列调用.因此,对于不使用此函数的结构,此函数运行时成本为零.

如果编译器实现者担心内联不会在可应用时消除这些调用,则可手动消除无操作子树.可在*__后移动*最前加:

静如(!hasElaborateMove!S);

在内部成员有后移动操作定义情况下,这种附加会产生些编译时开销,因为在递归下降时多次扫描子树.但,无论编译器优化能力,都保证了零运行时间成本.

定义后移动操作的结构管理自己的成本.

对d标库的影响

该建议对d标库影响很小.即使后移动操作是为结构定义的,上面详细介绍的编译器处理也可确保对d标库影响不大.

例外是:

  1. 在std.algorithm中定义的move族函数要加上*__后移动*.
  2. 要更新依赖精度实现swap函数.
  3. std.特征中要加新模板hasElaborateMove.如果结构或其任何成员定义了后移动操作,则必须返回.

重大变更和弃用

提案本身没有引入重大更改,因为结构类型必须明确选进此更改才能看到变化.

该提案确实加了两个具有特殊含义的函数.其中一个在保留空间中,因此不会破坏任何东西.除非已定义一个后移动操作,则切换到支持此DIP的实现将破坏旧代码,没法.
由于D程序员通常都知道op*函数是针对运算符重载的,因此问题应该不常见.

讨论区

有人说,由于D采取了按值传递非POD结构作为函数参数的方法,该提案可能需要更改D ABI.DIP作者表示同意.

一种批评是,建议的函数破坏了@安全.DIP作者说,唯一不安全是移动常/不变的构,而DIP已提倡去掉.此时,需要@系统或@信任.

最终审查讨论区

审查本dip时,在开发"复制构造器"的相关DIP.有人担心冲突.DIP作者说,与DConf的语言维护人员交流时确认无冲突.

一位审阅者问,如果程序员在类中定义后移动操作函数会怎么样,DIP的作者说,今天应忽略它,以后不确定.

一个反对要求后移动操作不抛的,理由是薄弱的"可能混淆".DIP作者建议可将要求放宽为"很好的建议",因为D的移动"无处不在",要阻止它这样做是"不可能的".

有人指出允许重载后移动操作,顺带引出用@禁止注解后,该干啥的问题,共识是,如这样,移动对象会导致编译错误.

有人提出在后移动操作问题.DIP作者最终建议应按类似当前对opAssign类似方式处理.

有人反对DIP提出方式,即指定编译器应该做什么及该如何表现,例如"编译器必须调用".DIP作者暗示愿意放弃这种术语,而指定确切结果,即编译器应表现如此.

有人建议DIP应指定复合结构的调用顺序.DIP作者回答说,该提案仅要求(成员的后移动操作必须在实例自身前面调用).

正式评估

语言维护者同意"这不是我们想要的DIP,而是我们需要的DIP",并说出现了许多问题,证明了这种需求.例如,他们引用了最近(在撰写本文时)C++的std::string中使用内部指针的发现.

发布了378 篇原创文章 · 获赞 25 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/fqbqrr/article/details/104318601