实施“延期”

2021-02-25 19:41:37

延迟声明已成为主流。 Go有它自己的特殊延迟,该延迟仅在功能端触发,否则延迟在范围端执行一致。语义。 Swift,Zig,Jai,Nim和Odin都以这种方式使用defer。尽管虚拟函数的存在使事情变得复杂,但实现延迟的问题类似于在C ++中为堆栈分配的对象实现析构函数。我找不到任何人描述延迟在其他编译器中是如何完成的,因此在为C2开发它的一个版本时,我必须随身携带它。出于后人的缘故,我认为撰写延迟的实现方式可能很有趣。首先,有许多不同的可能规则要适应延迟。本文的原始草案将处理所有延迟的goto。 C2保留了goto,C3保留了很长时间,因此这对于使文章完整很重要。但是goto推迟了很多工作,使文章既长又难于理解。因此,我们将限制自己返回,继续,中断以及后者的标记版本。如果有兴趣,我可以在另一篇文章中详细介绍如何为goto添加defer。延期的第一个问题是提早退出:每个退货都需要在末尾内联延期,因此降低为:对于中断和继续,此处理与回程类似,但是只有部分延期会在退货点处被内联。中断:void test(){延迟printf(" A"); while(true){延迟printf(" B"); {延迟printf(" C"); if(rand()%2 == 0)中断; }}}

以及内联版本:void test(){while(true){{if(rand()%2 == 0){printf(" C"); printf(" B");休息 ; } printf(" C"); } printf(" B"); } printf(" A"); }

我们也有标记的break版本。 (即使C3具有不同的变体,我也会在这里坚持使用Java样式标记的break语法)void test(){defer printf(" A"); FOO:while(true){延迟printf(" B"); while(true){延迟printf(" C"); if(rand()%2 == 0)打破FOO; }}}

再次降低为:void test(){FOO:while(true){while(true){if(rand()%2 == 0){printf(" C"); printf(" B");打破FOO; } printf(" C"); } printf(" B"); } printf(" A"); }

因此,正如我们所见,足以保留一个延迟列表,然后在遇到中断,继续或返回时以相反的顺序内嵌延迟语句。因此,现在我们列出了需要解决的所有问题。我们如何将它们放在一起?这是我使用的算法:这样,我们就可以将每个defer用作链接列表的顶部:简单。我们引入了一个辅助函数来内联延迟:void codegen_defers(Defer * current,Defer * last){while(current_defer!= last){codegen(current_defer); current_defer = current_defer-> prev_defer; }}

好吧,现在我们完成了吗?如果我们想超越C语法,则可能不完全。我们可以想象一下看起来像这样:在这种情况下,我们实际上有两个作用域:一个内部作用域(在{}之间)和一个在条件条件下开始的外部作用域。原理是相同的,因此我们可以重复使用与上述相同的解决方案,但是值得注意这种情况。我们还有其他问题要回答。该代码的作用:有人建议将其视为:我强烈反对该想法,因为这将意味着复合语句突然不同于常规语句。延迟可以做的另一件有趣的事情是,一个函数可能包含一个隐式的“延迟”,这个延迟被添加到调用它的作用域中。 Odin使用“延迟属性”具有此功能。 (请参阅此链接的进一步内容)。这很容易与延迟机械配合使用。使用延迟处理goto有点复杂,因为需要有条件地调用延迟:void test(int x){if(x> 0)goto FOO; //什么时候调用?延迟printf(" A"); FOO:printf(" B"); }

降低的代码应如下所示:void test(int x){bool _defer_1 = false;如果(x> 0)转到FOO; _defer_1 = true; FOO:printf(" B");如果(_defer_1){printf(" A"); }}

由于B可以跳入范围,也可以跳出范围,因此这为分析增加了另一个维度。解决方案并不困难,但绝对不能像break和continue的结构化跳转那样简单,根本无法处理setjmp的非本地跳转。 Go的延迟方式有所不同。 Go的延迟实际上将延迟代码存储为像闭包那样在函数端而不是作用域端排队和调用的闭包。这意味着延迟实际上需要为其分配内存。这样的循环:将循环中生成的所有延迟排入长列表,并在函数末尾释放它们。如果`defer`释放了一些限制,例如数据库连接,那么这是个坏主意。对于各种" gotcha"在Go中,由于这种延迟样式,请参见此内容。尽管Go延迟程序可以很好地与异常和`goto`配合使用,但它有很多怪癖,也需要保留内存来存储延迟程序。有时,人们宁愿延迟仅在错误时发生:File * getAndCheckFile(){File * f = getFile();如果(!f)返回NULL; //如果返回错误,我们想关闭。延迟关闭(f);如果(!fileIsValid(f))返回NuLL;如果(readHeader(f)!= 0xdeadbeef)返回NULL; //哎呀,我们将关闭f!返回f; }

因此,Zig引入了errdefer,而C3则延迟了catch / defer try语句。作为特殊形式的延期的替代(和补充),可以取消延期。到目前为止,我只看到延迟实现为RAII的此功能。从理论上讲,它可能类似于:File * getAndCheckFile(){File * f = getFile();如果(!f)返回NULL; FOO:延迟关闭(f);如果(!fileIsValid(f))返回NuLL;如果(readHeader(f)!= 0xdeadbeef)返回NULL;延期FOO;返回f; }

对于缺少final和RAII的语言,Defer是有用的功能。通过结构化跳转,可以轻松实现零开销的实现。