头文件和预处理器-不能与它们共存,不能没有它们

你好,我叫理查德·鲁索。 我是Visual C++编译器前端开发团队的最新成员,从一月开始。 我对微软并不陌生,因为我花了三年时间在WindowsVista上。 我很高兴能加入前端团队,因为编译器开发是我几年来的爱好。

null

这个博客上的大多数帖子都讨论了添加的新功能,或者在不同的职位上的日常工作是什么样的。 相反,我想讨论一下我对C/C++的一个特殊方面的看法,我认为应该得到更多的设计注意:头文件和预处理器。 我可能会在这里讨论一些假设的特性,我只是想说,这并不一定意味着VisualC++团队将致力于或提供这些特性。 在写这些想法的时候,我主要希望做的是引发讨论并得到你的反馈。 应该有一个链接下面留下评论,所以请做!

首先,头文件的真正用途是什么? 好吧,不必研究Kernighan和Ritchie的设计原理,它最基本的目的就是让我们在使用前声明语言中维护代码单元以进行单独编译。 你可以想象,如果没有预处理器,我们将不得不在每个源文件中声明相同的东西-我们很快就会厌倦这一点,可能会把一些看起来很像当前预处理器的东西拼凑在一起。 当然,我们使用预处理器处理很多聪明的事情,但对我来说,这是它的基本目的。

头文件有什么问题? 好吧,他们似乎有助于真正长的建设时间。 你曾经使用过cl.exe的预处理器模式吗? 您可以使用/E(对标准输出的预处理)、/P(对文件的预处理)和/EP(对文件的预处理)访问这些命令,但不生成#行指令。 试一试,例如“cl/pfoo.cpp”将产生“foo.i”。 在我的系统上,我用MessageBox函数快速编写了一个Windows“helloworld”。 当我对这个5行程序进行预处理时,编译器需要解析大约200000行源代码。 现在想象一下,如果每个源文件都包含windows.h,那么在您的项目中会是什么样子。 您可以想象,解析20万行声明会让编译器慢一点。 还有什么? 嗯,头文件似乎是多余的。 我们必须输入类名、方法名、参数列表等两次。 虽然这并不是开发C++项目最昂贵的部分,但它可能会稍微降低你的思维过程。 它还创建了额外的维护。 如果在一个地方更改参数列表,则需要在另一个地方更改它。 再说一遍,这不是一个巨大的成本,而是一个真实的生活成本。 我们还没有完成-我可以补充一些潜在的问题。 头文件的变化取决于它们被预处理的上下文——或者更简洁地说,它们有隔离问题。 如果您正在集成两个库,它们都有一个foo.h,并且都使用保护宏fooh,会怎么样? 嗯,这是你作为一个程序员必须花时间去处理的事情。 同样,如果您有一个没有经过仔细设计的头的大项目,您可能会注意到编译时(甚至可能是运行时)行为的差异,这取决于包含头的顺序。 这不是世界末日,但你要付出代价来调查和解决问题。 我认为大多数C/C++程序员会同意预处理器附带一个价格标签和一长串潜在的陷阱。

嗯,还不错吧? 当然。 我想到的第一个好处是我最感兴趣的。 我们在C/C++中有一个情况,在使用之前我们必须声明,并且我们把所有这些共享声明放在头文件中。 我们可以很容易地以这种方式生成相互依赖的独立编译单元。 但是由于头文件满足了相互依赖性,所以通常我们所有的源文件都可以并行编译。 我想大多数人都会同意这种平行性是一件好事。 看看反例。 假设你正在编译一个C程序。 首先,您可能很少一次编译一个源文件。 将源文件集合传递给编译器,就像将这些文件传递给C/C++编译器一样。 但是C编译器必须以不同的方式处理这批文件。 C/C++编译器可以单独编译它们,然后最后将它们看作是用于链接目的的单元。 对于C#情况,编译器必须同时考虑所有这些源文件,因为它们可能具有相互依赖性: 您可以引用另一个源文件中的类,而不必首先给出它的声明。 简而言之,在C/C++中,翻译单元总是单个源文件和报头,而在C语言中,翻译单元可能是多个源文件。 至少,这使得C构建过程的并行化看起来更加困难。 毫无疑问,这是可行的,但是会有一些开销与此问题相关。 你可以认为这是通过维护头文件来支付开销的C/C++编码器。 毫无疑问,头文件还有其他好处。 例如,您可以使用宏预处理器为我们的语言添加基本语法,以帮助“自动化”某些任务。

我们能做些什么? 我要讨论两个建议。 其中一个根本不是我的,并且考虑C++的模块。 另一个可能是更实用的工具,可以帮助您诊断代码库中头文件的问题。

“module”这个词有很多不同的含义,甚至在编码世界的上下文中也是如此。 我想说,许多编程系统(注意这不包括C和C++)提供了模块功能,这是一些单独的编译和命名空间概念的加入。 C和C++给我们提供了单独编译的工具,C++给我们提供了命名空间,但它没有给我们一个统一的特征,将它们封装在一起,允许我们引用单独编译的模块并从中导入选择的符号。 为C++建议输入Vandevoorde模块(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2073.pdf). 这为我们提供了一种机制,通过这种机制,我们可以引用以前可能编译过的模块,而不是在项目中包含声明的源代码。 这在上面指出的警告方面有一些重要的好处;例如,它确实具有隔离属性,并且没有与头文件冗余相关的维护负担。 如果以某种方式使用,我可以看到这个特性的使用会对parallelism属性产生负面影响。 例如(与C#情况类似),如果项目中的每个源文件都是一个单独的模块,那么编译器在尝试并行编译这些文件之前必须分析依赖关系。 幸运的是,“import”语句为它提供了一些关于这些依赖关系的线索,并且可能会被快速扫描,因此毫无疑问,有一些方法可以解决这个问题,但是会有一些开销。 我没有太多的东西要补充范德沃德在那篇论文中的讨论,它是非常彻底的,如果你对这样一个模块系统的设计和潜在的问题感兴趣,我鼓励你阅读它。

考虑重新设计世界虽然很有趣,但在不改变头文件的基础结构的情况下,我们今天可以做些什么来让事情变得更好呢? 我可以设想生成头文件或扫描头文件和更大的代码库的工具,并尝试为您诊断问题。 我在上面所说的头文件问题中的大部分都与人工程序员的工作成本有关。 嗯,在某些情况下,自动化工具可能有助于降低或消除成本。

第一个建议是“自动”生成头文件。 考虑编译器,分析C和/或C++源,并将刚才的声明提取到最小的头文件中。 您可能有一个包含windows.h的源文件,然后声明几个类。 头文件需要包含这些类的声明,可能只包含windows.h中声明的几个typedef和它包含的各种头文件。 结果很可能很短;也许您只需要声明windows.h中的一些typedef,比如HMODULE、HWND等。 此工具可以潜在地集成到IDE中,这样当您更改源文件时,它会在必要时自动更新头文件。 为了提高效率,该工具可以假设像windows.h这样的系统头不会改变。 另一个提高效率的建议是让工具为整个库而不是单个翻译单元级别生成一个标题。

这样的工具似乎有可能帮助解决维护和构建时间问题。 整个windows.h头文件需要由头文件生成工具进行解析,但这一成本是分摊的。 它只在您修改源文件时支付,并且您每次编译包含关联头的另一个源文件时都会获得奖励。 我在这方面看到了潜在的问题,但是如果您有一个以正确的方式划分的代码库,您甚至可以签入那些生成的头并在整个开发团队中节省更多的编译时间。

其他问题呢,比如隔离? 头生成工具可以通过创建“行为良好”的头文件来帮助解决其中的一些问题。 例如,它可能会生成预处理器代码,用于保存和还原文件上下文中宏的状态,或者生成不使用任何预处理器功能的头文件,除了文件开头和结尾的保护宏。 但是更有用的是一些分析邮件头的工具。 我将举几个简单的例子。 它可能会查找隔离问题,例如INCLUDE路径中的两个不同头文件具有相同的文件名或使用相同的guard宏。 它可以找到没有正确的保护宏的头。 它可以构建一个依赖关系图,显示一个头的定义如何影响另一个头的定义,以及特定头和源文件的include图是什么样子。

这样的分析工具也可以帮助您缩短构建时间。 例如,它可以扫描您的源文件并告诉您不需要包含特定的头,因为没有使用任何声明。 或者它可能会告诉您,不包括foo.h(其中包括bar.h),您可以只包括后者,因为这个特定的源文件只使用bar.h中的声明。

恐怕我已经用完了这个职位的时间和空间,但我希望这是一个有趣的阅读,并让你思考什么关于预处理器和头文件的功能可能会使你在C和C++代码库中更有效率。 请留下评论,如果你喜欢这些想法,不喜欢他们,或有自己的。

感谢阅读!

理查德

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享