65.9K
CodeProject 正在变化。 阅读更多。
Home

包含小工具

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2013 年 3 月 13 日

CPOL

5分钟阅读

viewsIcon

16216

包含保护、Pragma Once、前向声明以及其他在处理包含时可能有所帮助的技巧。

包含保护、Pragma Once、前向声明以及其他在处理包含时可能有所帮助的技巧。

在 C++ 中,我们可以使用文件包含做什么?我们需要在 *每个* 文件中 *始终* 包含项目中的 *所有* 其他头文件(以及第三方库)吗?当然,必须有一些规则来妥善管理这个问题。

本文讨论的问题当然并非新鲜事。每个 C++ 程序员都应该知道如何正确使用 #include。但不知何故,我仍然看到大量代码混乱不堪,编译时间过长……更糟糕的是(在大多数其他情况下也是如此),即使你花了一些时间尝试遵循一些良好的 #include 策略,一段时间后混乱仍然会从文件中滋生。当然,我也对这类错误负有责任。

问题是什么?

为什么最小化头文件和 include 语句的数量如此重要?

这是一张通用图

你看到了答案吗?当然,你的程序可能有更复杂的结构,所以再添加 100 个文件并将它们随机连接起来。

C++ 编译器处理头文件的过程

  • 读取所有头文件(打开文件,读取其内容,如果发生错误则列出错误)
  • 将头文件的内容“泵入”到一个翻译单元
  • 解析并获得头文件内代码的逻辑结构
  • 需要运行旧的 C 宏,这甚至可能改变文件的最终结构
  • 模板实例化
  • 大量涉及 string 的操作

如果存在太多冗余,编译器需要花费更长的时间工作。

有什么指导方针吗?

  • 到处使用前向声明!
    • 尽量在任何可能的地方使用它们。这将减少包含文件的数量。请注意,在需要某个类型(在函数中、作为类成员)时,包含文件对于编译器来说可能并非如此关键——它只需要知道其名称,而不需要完整的定义。
  • 头文件顺序
    • 文件 *myHeader.h*(包含一些类)应该首先包含(或者紧随通用预编译头文件之后),并且是自包含的。这意味着当我们在项目的其他地方使用 *myHeader.h* 时,我们不需要知道它有哪些额外的包含依赖项。
  • 速度
    • 现代编译器在优化头文件访问方面做得相当不错。但来自我们这边的额外帮助也会很有益。
    • 预编译头文件可以节省生命和时间。尽可能多地放入系统和第三方库的头文件。不幸的是,当需要跨平台解决方案并且包含过多时,事情可能会变得棘手。在此阅读更多:gamesfromwithin
    • Pragma Once、Include Guards 和 Redundant Include Guards:在选择最佳组合方面没有明确的赢家。在 Visual Studio 中,PragmaOnce 似乎很棒,但它不是一个标准化的解决方案。例如,GCC 通常在标准 Include Guards 方面表现更好。
  • 工具
    • 找到任何你喜欢的工具,并为特定的 C++ 文件生成依赖关系图。
    • 一个可能很有用的快速工具是 Visual Studio 的选项 /showincludes链接),它(顾名思义)会打印出所有包含到 C++ 源代码中的内容。如果列表太长,也许应该看看特定的文件。在 GCC 中,还有一个更高级的选项 -M链接),它显示依赖关系图。

正如我们所见,通过使用指针或引用作为成员或参数声明,我们可以大幅减少包含的数量。总的来说,我们应该只包含编译文件所需的最小文件集。甚至有可能将这个数字减少到零。

理想情况下

#ifndef _HEADER_A_INCLUDED_H
#define _HEADER_A_INCLUDED_H

class A
{
};

#endif // _HEADER_A_INCLUDED_H

以及在源文件中

#include <stdafx.h> // precompiled if needed
#include "A.h"

#include "..."  // all others

// implementation here

有希望吗?

头文件可能非常成问题,而且这绝对不是 C++ 语言的一大特性。如果包含过多,编译时间会越来越长。而且很难控制。但还有其他选择吗?其他语言如何处理类似的问题?

很难将 Java 和 C# 的编译与 C++ 相提并论:C++ 生成针对特定架构优化的本地二进制代码。托管语言编译成某种中间语言,这比本地代码容易得多。值得一提的是,托管语言使用模块(而不是包含),这些模块几乎是编译代码的最终版本。这样,编译器就不必一遍又一遍地解析模块。它只需要获取所需和已编译的数据和元数据。

因此,看来 C++ 的主要问题在于缺乏 **模块**。这个想法可以减少翻译单元创建时间,最小化冗余。我以前已经提到过:通过 clang 实现 C++ 的模块(这里这里)。另一方面,C++ 编译非常复杂,因此引入它并不容易,更重要的是,**标准化** **模块**概念。

链接

  • 链接到有趣(且更普遍)的问题:为什么 C++ 编译要花这么长时间
  • John Lakos 的《Large Scale C++ Software Design》——我在上一篇关于封装的文章中提到了它。书中详细讨论了 C++ 代码的物理结构。建议所有 C++ 程序员阅读。
  • even-more-experiments-with-includes - @Games From Within
  • RedundantIncludeGuards - 一种简单的技术,在包含某个内容之前,只需检查其包含保护是否已定义。在旧编译器中,这可以提高性能,但在现代解决方案中,使用它的好处并不那么明显。

待续...

在不久的将来,我将尝试在这里发布一些关于编译时间和 #include 技巧的基准测试。

© . All rights reserved.