翻译单位被认为有害吗?

2020-10-08 09:13:18

但是,你的朋友托尼告诉你要使用更多的函数,所以你就这么做了。

整数区域(正方形%s){返回宽度*宽度;}整数宽度(正方形%s){返回%s.width;}。

区域是您真正关心的函数,它首先被定义-毕竟,代码是从上到下读取的。

您可能已经猜到,在结构的左方括号之后,上面的代码是用D编写的。我想我的读者并不是真的喜欢D,所以您可能更喜欢一些Rust?

Pub FN Area(Square:Square)->;I32{Return Width*Width(S)}Pub FN Width(Square:Square)->;I32{Return s.width}Pub Structure Square{width:I32}。

函数面积int{返回宽度*宽度;}函数宽度int{返回s.width}类型正方形结构{width int}。

函数区域(s:Square)->;Int{return width(s:s)*width(s:s);}函数宽度->;Int{return s.width}结构正方形{var width:int=0;}。

但是,当然,您会担心开销,并且会想要性能最好的语言(这不是一个单词)。为了取悦和打动我,让我复制D代码并添加那个非常重要的分号。

结构正方形{int width;};int area(Square S){Return Width*Width;}int Width(Square S){Return s.width;}。

那很好,不是吗?有趣的是,大多数语言看起来都很相似。嗯,等等,这不管用?!

但是,你这个笨蛋,它就在那里。我把全球范围内的一切都像疯子一样申报了,难道你看不出来吗?

在作为命名空间N成员的函数的定义中,在函数的声明符id23之后使用的名称应在使用它的块或其封闭块之一([stmt.block])中使用之前声明,或者应在命名空间N中使用之前声明,或者,如果N是嵌套命名空间,则应在其在N个封闭命名空间之一中使用之前声明。

当然,这是没有意义的,编译器真的可以很容易地独立于定义解析声明,其他语言也证明了这一点。或者你知道的,C++类。(想象一下用一个充满静态方法和嵌套类型的类替换一个大的命名空间),当然,除非是性能方面的问题。但是,您是一个非常伟大的工程师,所以您不会让一个源文件增长到几百行以上,对吗?我打赌您的代码一定很漂亮,就像这个自包含的超级有用的小程序。

在我的系统上可以扩展到大约33000行代码。奇怪的是。但稍后会有更多关于这一点的报道。

让我们回到原点。C++以其无穷的智慧让我们向前声明函数,因此我们可以编写以下代码:

结构正方形{int width;};int width(常量正方形&;s);int area(常量正方形&;s){return width*width(S)}int width(常量正方形&;s){return s.width;}

除了要求您完全正确地声明函数-这很难维护,许多实体都不是可向前声明的,特别是类型别名、模板化类型等。这是一个奇怪的限制,因为向前声明函数需要您知道精确的签名,而您只是试图引入一个名称。

你会注意到Area从不抛出,也就是说,没有可以抛出的Area的子表达式。

不可避免的是,这一点失败了。错误:静态断言失败。我们确实忘了告诉编译器我们的函数不能抛出。

Int width(Const Square&;s)无例外;int Area(Const Square&;s)无例外{return width(S)*width(S);}int width(Const Square&;s)无例外{return s.width;}。

注意,我们需要在所有声明上添加NOEXT EXCEPT,包括转发声明。而且,您可以很容易地欺骗编译器。

Int Area(Const Square&;s)不除{Return Width(S)*Width(S);}Int Width(Const Square&;s){掷42;}。

上面的代码将std::Terminate(),您知道编译器知道这一点,每个人都知道这一点。

所以…。哪些函数应该标记为NOEXT?实际上很简单。所有不能抛出的函数,那就是:

因此,作为一个努力标记所有不能抛出的函数的开发人员,您必须递归地遍历调用树,直到您可以确定调用链永远不会抛出或者实际上可能抛出为止(因为一个被调用者确实抛出了,或者处于C接口边界,等等)。反对异常的一个论点是,它使关于控制流的推理变得更加困难:异常或多或少会迫使您每次都对整个程序的控制流进行推理。没有什么能解决这个问题,但是,要肯定地说出这个关键字,你仍然需要做那个分析。如果你写泛型代码,你必须告诉编译器一个符号也不例外,如果它的所有子表达式都是手动的。

而且编译器不能信任您函数确实不会抛出,因此实现者将在这里和那里注入对std::Terminate值的调用,这在一定程度上否定了将函数标记为NOBER的性能好处。

AUTO WIDTH=[](Const Square&;s)->;int{Return s.width;};Auto Area=[](Const Square&;s)->;int{Return Width(S)*Width(S);};

事实证明,编译器非常擅长知道哪些函数不例外,在lambdas的情况下,在任何调用之前,编译器总是可以看到定义,所以它可以隐式地将其标记为NO EXCEPT,并为我们做这项工作。这允许作为C++20的一部分。

我并不是说没有例外在理想的世界里是没有必要的,因为它有不止一个含义,而且人们使用它的方式也不同。值得注意的是,NOEXCEPT可能意味着:

第一条语句是对编译器的请求,第二条语句是对编译器和人类读者的断言,而最后一条语句是专门针对人的。

因此,即使编译器可以自己决定函数是否是真正的非抛出函数,在API边界作为人与人之间的契约时,没有例外仍然是有趣的。

如果表达式包含以下任何内容作为可能求值的子表达式(3.2[basic.def.odr]),则该表达式是事务不安全的:

创建限定易失性类型的临时对象或子对象限定易失性类型。

函数调用(5.2.2 Expr.call),其后缀表达式是命名不是事务安全的非虚拟函数的id表达式。

函数类型不是“TRANSAGE_SAFE Function”的任何其他函数调用。

细节并不重要,但基本上,TRANSACTION_SAFE表达式是一个不接触易失性对象的表达式。并且只调用具有相同属性的函数。这可能超过99%的函数-我怀疑出于兼容性原因存在非常可怕的缺省值。重要的部分是您必须标记所有函数或者希望属性递归保持为真。(就像NOEXT,您可以通过将函数TRANSAFE标记为TRANSAGE_SAFE来撒谎,即使被调用者本身不是TRANSAGE_SAFE,这也为UB打开了大门)。

常量表达式函数略有不同。编译器知道哪些函数是候选常量。大多数情况下,无论它们是否被标记为候选常量,编译器都会对它们进行常量求值。关键字是确保编译器在可能的情况下实际执行常量求值所必需的,最重要的是,因为删除函数的常量可能是一种破坏源代码的更改-(如果在计算conexpr变量的过程中调用了该函数)。根据其本质,conexpr意味着在某个地方定义的conexpr函数是TU。而且所有在TU中没有定义的东西都不能进行常量求值。C++20的一项建议建议在某些情况下将其隐式。

目前,我们只剩下以下代码,您可以使用适当的限定符。

Conexpr int width(Square S)除TRANSACTION_SAFE外无;conexpr int Area(Square S)NOEXT TRANSACTION_SAFE{return width(S)*width;}conexpr int width(Square S)noexext TRANSACTION_SAFE{return s.width;}。

从C++20开始,conexpr函数可以抛出。委员会还在考虑在23或26岁之前制定新的表达式,所以我们正在慢慢地达到一个95%以上的函数既是恒定的,又不是异常的,并且必须手动标记的地方。

源文件及其包含的头形成一个翻译单元,多个翻译单元形成一个程序。

头文件和源文件是我们自欺欺人的,据我所知,“头”一词只出现在标准中,用来命名“标准库头”,而且在实践中,头不一定是真正的文件,它们标识的是编译器可以理解的东西,即一系列令牌。

在实践中,我们使用预处理器-60年代末、70年代初LSD上的一个喝醉了的贝尔实验室实习生实现的一项技术-将我们永远不太确定它们来自系统中的哪个位置的文件集合缝合在一起。我们称它们为头文件和源文件,但实际上,您可以在.h中包含一个.cpp文件,或者选择使用.js扩展名作为头文件,将.RS扩展名用于源文件,而您的工具不会在意。当然,您可以创建循环头依赖项。

预处理器是如此愚蠢,以至于你必须明确地告诉它,它已经用最糟糕的模式--包含保护模式--包括了哪些文件。这本来是可以解决的,但是你看,这并不是因为有些人担心将他们的工作区的几个部分硬链接在一起。

最后,#include指令的工作方式与cat类似--只不过cat更适合它的工作。

当然,因为任何东西都可以在任何地方定义宏,所以任何“头”都可以在编译时以一种混乱的方式重写所有代码(在这里,混乱意味着确定性,但远远超出任何人的认知能力)。

在这个上下文中,很容易理解编译器为什么不向前查看几万行,看看您是否声明了引用的符号。嗯,这是一个足够好的理由吗?我不知道…。但是,结果(我认为这不是真正自愿的),重载和名称查找只能作为最佳匹配,而不是最佳匹配。

常量表达式int f(双x){返回x*2;}常量表达式自动a=f(1);常量表达式int f(Int X){返回x*4;}常量表达式自动b=f(1);

如果你既不是错的,也不是惊恐的,那么你可能患上了斯德哥尔摩综合症。而且,因为声明的顺序可能会影响程序的语义,而且宏可以重写所有内容,所以C++也没有解决办法。

通常的做法是将声明放在头文件中,而将实现放在源文件中。这样,包含同样数十万行头文件的非常小的源文件的编译速度会更快。至少它们的编译频率会更低。我们还早于大多数代码可以是constexpr,并且constexpr声明必须对所有转换单元都可见。所以,看着总是使用auto的模板化、概念化常量扩展代码,您会想知道可以将什么拆分成一个源文件。可能什么都没有。我想除非您坚持使用C++98,或者广泛使用类型擦除,例如,您可以使用SPAN,这是C++20提供的最好的类型。

然后,当然,链接器将获取各种翻译单元,并将其编成一个程序。此时,臭名昭著的“一个定义规则”开始起作用。您只需定义每个符号一次。数百个标题以不同的顺序扩展到数十万行代码,并以特定于该项目的方式定义各种宏集。当天,系统上不应重新定义任何内容。最好的情况是,您会收到链接器错误。更有可能的是,你得了不治之症。您的代码现在是否在某种程度上违反了ODR?ODR是编译器不知道代码库中存在什么名称的直接结果。

事实证明,泰特斯·温特斯在伟大的新演讲“C++过去与未来”中详细讨论了ODR,您一定要看看这个。

他们可以创建静态库-基本上是一个带有多个翻译单元的zip。当使用该库时,链接器可以方便地不链接其他未引用的静态对象。他们没有得到构造函数可能有副作用的备注。

他们还可以制作动态库。我们仍然相信的最糟糕的想法是:制作动态库也许可以逃脱惩罚。它可能会工作,也可能不会,您在运行时就会知道。

它们可以优化整个程序,因为与编译器不同,链接器可以看到您的所有代码。所以,您非常小心地以非常复杂的构建系统为代价拆分成多个源文件的所有代码,最终都是由链接器缝合在一起的,并以这种方式作为一个整体进行了优化。

当然,您可以在分布式构建场中并行运行大量构建,其中您所有的大量CPU都在同时解析<;Vector>;。另一方面,编译器本身期望您同时运行多个作业,不会在其实现中实现任何类型的并发。

然后丢弃从main()函数或全局构造函数开始的调用图中没有使用的内容。

您可能会问什么是C++模块?标准化的预编译头是什么是模块。你得到简化的二进制形式的“头”,这使得编译速度更快。假设你不需要一直重新构建所有东西,我怀疑如果你在头中实现了大型第三方,它们真的会很有帮助。当工具设计如何处理模块时。

请注意,我相信修改模块接口会被动地修改所有模块接口,即使您不修改现有的声明也是如此。

我想他们可能是。模块是封闭的,在解析定义之前考虑同一模块中的所有声明似乎是合理的,但这会使“移植到模块”变得更加困难,而“移植到模块”是TS的重要部分。除非您想就此撰写一篇论文?!

有一种强烈的动机是让模块在20yo代码库上工作,而不实际在其中投入任何工作,因此,当前的建议允许您或多或少地在您想要的任何地方声明和使用宏,并可能从模块导出它们,这是…。我对此有一些看法。也就是说,我认为模块代码库实际上是如何高效构建的还有待观察。

已经有一些建议禁止或修复模块上下文中的某些特定构造,我认为它们不会很好,这是因为人们更关心现有的代码库,而不是未来的代码。在这些情况下,Python2经常被用作警示故事。

作为美化的编译头,C++模块并不努力取代翻译单元模型。模块仍然被拆分为其接口(编译器可以将该模块的源代码转换为BMI-二进制模块接口-),以及接口中实现的东西的定义(目标文件)。实际上,以下代码将不会链接。

//m1.cppm导出模块M1;export int f(){return 0;}//main.cpp import M1;int main(){f();}clang++-fmodule-ts--预编译m1.cppm-o m1.pcmclang++-fmodule-ts-fmodule-file=m1.pcm main.cpp

因为M1模块二进制接口不会考虑f()的定义,除非您将其标记为内联,或者从中构建一个.o。尽管如此,我的系统上的BMI确实包含函数的定义,因为更改它也会更改BMI。无论如何都会导致所有依赖项的重建。

因此,模块不像在其他语言中那样是一个自给自足的单元,幸运的是,它们确实要求在单个翻译单元中完成给定模块的实现。

人们认为他们的代码是一个有凝聚力的整体,通俗的术语是“项目”。编译器看到你的代码越多,它就能对它进行越多的优化。越来越多的C++结构需要在任何时候对编译器都是可见的。ConstExpr方法、模板(和概念)、λ、反射…。

然而,编译模型鼓励我们让我们的工具无助地盲目,使我们的生活变得更加艰难,这些问题的解决方案并不是微不足道的。

一个核心问题是,无论程序是用哪种语言编写的,它都是定义的集合,但是开发工具操作文件,并且存在一定的不匹配。

很长一段时间以来,C++社区一直坚信定义和声明的分离、源/头模型更优越,但是我们看到越来越多的只有头的库,它们的编译速度可能会稍慢一些,但最终更易于使用和理解。对于人,对于工具,对于编译器,如果将来作为模块发布的库也将是“仅模块接口的”,我不会感到惊讶。我认为单头库作为一个文件发布并不重要。重要的是,它们可以通过包含单个文件来使用,它表示“这是构成我的库的一组声明”。

当然,我们不应该轻而易举地解决编译时间过长的问题,但众所周知,大多数FX/3D艺术家需要一台4000美元或更多的机器才能完成他们的工作。工作室明白这是做生意的成本,也许编译C++也需要昂贵的硬件。也许这没什么大不了的。硬件便宜,人不便宜。尤其是优秀的软件工程师。

我不知道我们是否会设法摆脱目标文件、静态库和动态库,我不知道我们是否会停止关心非常具体的库之外的ABI。

但是,由于C++社区梦想有更好的工具和依赖项管理器,也许更准确地定义基础会有所帮助:我们的程序是一组定义,其中一些是由其他人提供和维护的。我认为我们的工具越紧密地坚持这个模型,从长远来看,我们就会过得越好。

因此,我们可能需要询问有关编译模型的基本问题和我们持有的检查信念(例如,“编译器和构建系统需要保持分离”。是吗?在多大程度上?)。

绝对有巨大的技术障碍,社会和法律障碍(LGPL,你应该为自己感到羞愧)。这看起来不可能,但回报会是如此之大。与此同时,我完全意识到我没有任何答案,我会在互联网上大喊大叫。