比约恩·法勒最近写了一篇博客文章,展示了如何在C++17中实现编译时快速排序。这是一个巧妙的演示,它使用不断演进的C++特征集编写代码,虽然不是很简洁,但比以前的迭代更加流线型。他总结道,“……这项技术的实用性非常有限,但有点酷,不是吗?”
在编译过程中对代码进行评估是非常有用的。凉爽(有很多)来自于随之而来的可能性。从比约恩的例子开始,这篇文章开始教授D编程语言中编译时评估的几个有趣方面。
这篇文章引起了我的注意,是因为Russel Winder在D论坛上提出了一个挑衅性的问题,“D肯定能做得更好”,安德烈·亚历山德雷斯库(Andrei Alexandrescu)用“没有故事”式的回答很快就解决了这个问题。“真的没什么可做的。只需使用标准的库排序,”他打趣道,接着是代码:
void main(){
虽然对于不熟悉D的人来说,这可能并不明显,但对排序的调用实际上是在编译时发生的。让我们看看原因。
这是真的。要在D中的编译时运行,不存在任何障碍。任何编译时函数也是一个运行时函数,可以在任何上下文中执行。然而,并不是所有的运行时函数都符合CTFE(编译时函数评估)的要求。
CTFE资格的基本要求是,函数必须是可移植的,没有副作用,不包含内联程序集,并且源代码必须可用。除此之外,决定一个函数是在编译时求值还是在运行时求值的唯一因素是调用它的上下文。
为了在编译时执行,函数必须出现在必须如此执行的上下文中…
然后列举了几个例子来说明这一点。归根结底是这样的:如果一个函数可以在编译时上下文中执行,那么它就会在编译时上下文中执行。当它不能被超越时(例如,它不满足CTFE要求),编译器将发出一个错误。
void main(){
在这里,让CTFE神奇的兴趣点是第3行和第4行。
第3行中的枚举是一个清单常量。它不同于D中的其他常量(标记为immutable或const的常量),因为它只在编译时存在。任何试图获取其地址的行为都是错误的。如果它从未被使用过,那么它的值将永远不会出现在代码中。
使用枚举时,编译器基本上会粘贴其值来代替符号名。
enum xinit=10;
这里,x被初始化为文本10。这和写int x=10是一样的。常量yinit用int-literal初始化,但y用yinit的值初始化,虽然在编译时已知,但yinit本身不是一个literal。yinit将在运行时存在,但xinit不会。
在示例1中,静态变量b用清单常量a初始化。在CTFE文档中,这是一个示例场景,在该场景中,必须在编译期间对函数进行求值。函数中声明的静态变量只能用编译时值初始化。试图编译以下内容:
使用函数调用初始化静态变量意味着该函数必须在编译时执行,因此,如果它符合条件,就会执行。
这两个谜题,manifest常量和静态初始值设定项,解释了为什么示例1中的排序调用发生在编译时,没有任何元编程扭曲。事实上,这个例子可以缩短一行:
void main(){
如果不需要在运行时保留b,则可以将其设置为枚举,而不是静态变量:
void main(){
在这两种情况下,对sort的调用都将在编译时发生,但它们处理结果的方式不同。考虑到,由于枚举的性质,改变将产生一个等价于:WreLeln([ 0, 1, 2,3, 4 ])。因为对writeln的调用发生在运行时,所以数组文字可能会触发GC分配(尽管它可能会优化,有时也会优化)。对于静态初始值设定项,没有运行时分配,因为函数调用的结果在编译时用于初始化变量。
值得注意的是,sort并不是直接返回int[]类型的值。看一眼文档,你会发现它给你的是一个分类。具体来说,在我们的使用中,它是一个分拣机!(int[],";a<;b";)。这种类型与D中的数组一样,公开了随机访问范围的所有原语,但还提供了只在排序范围内工作的函数,并可以利用它们的排序(例如三等分)。该阵列仍在那里,但被包装在一个增强的API中。
我在上面提到过,所有编译时函数也是运行时函数。有时,它';它有助于区分函数本身内部的这两个方面。D允许您使用_ctfe变量来实现这一点。这里';这是我书中的一个例子,';学习D';。
字符串genDebugMsg(字符串msg){
msg pragma在编译时将消息打印到stderr。当genDebugMsg在这里被作为第二个参数调用时,那么在该函数中变量_ctfe将为true。然后,当函数作为writeln的参数调用时(这发生在运行时上下文中),_ctfe为false。
它';重要的是要注意_ctfe不是编译时值。没有函数知道它是否';在编译时或运行时执行。在前一种情况下,它是';它由编译器内部运行的解释器进行评估。即使这样,我们也可以在函数本身内部区分编译时和运行时值。然而,函数的结果将是编译时的值,当它';在编译时执行。
现在让';让我们看一看那些没有';t使用标准库中的现成功能。
几年前,安德烈出版了';D编程语言';。在描述CTFE的部分中,他实现了三个函数,可以用来验证传递给假设的线性同余生成器的参数。他的想法是,这些参数必须满足一系列标准,他在书中列出了这些标准(为评论购买——这很值得),以产生尽可能大的周期。这里是,减去单元测试:
//欧几里德算法的实现
这段代码的要点与我在本文前面提到的相同:properLinearCongressionalParameters是一个可以在编译时上下文和运行时上下文中使用的函数。那里';它不需要特殊的语法来工作,也不需要创建两个不同的版本。
要将线性同余生成器实现为模板结构,并将RNG参数作为模板参数传递吗?使用properLinearConcurentialParameters验证参数。想要实现一个在运行时接受参数的版本吗?适当的近似全等参数已经覆盖了你。想要实现一个可以在编译时和运行时使用的RNG吗?你明白了。
void main(){
如果你';我一直在注意,你';我们知道ctVal必须在编译时初始化,因此它会强制CTFE调用函数。对同一函数的调用与对writeln的参数的调用发生在运行时。你也可以吃蛋糕。
D中的编译时函数求值既方便又无痛。它可以与其他功能相结合,例如模板(它对模板参数和约束特别有用)、字符串混合和导入表达式,以简化可能非常复杂的代码,其中一些代码不会';在许多语言中,没有预处理器是不可能的。作为奖励,Stefan Koch目前正在为D前端开发性能更高的CTFE引擎,使其更加方便。请继续关注这方面的更多新闻。