【问题标题】:Clean up your #include statements?清理你的#include 语句?
【发布时间】:2010-11-04 02:50:31
【问题描述】:

如何在 C 或 C++ 项目中维护#include 语句?似乎几乎不可避免的是,最终文件中的包含语句集要么不足(但由于项目的当前状态而恰好可以工作),要么包含不再需要的内容。

您是否创建了任何工具来发现或纠正问题?有什么建议吗?

我一直在考虑编写一些东西,多次单独编译每个非头文件,每次都删除#include 语句。继续这样做,直到获得最少的包含集。

为了验证头文件是否包含他们需要的所有内容,我将创建一个源文件,它所做的只是包含一个头文件并尝试编译它。如果编译失败,则头文件本身缺少包含。

在我创造一些东西之前,我想我应该在这里问一下。这似乎是一个有点普遍的问题。

【问题讨论】:

标签: c++ file include header


【解决方案1】:

为了验证头文件是否包含他们需要的所有内容,我将创建一个源文件,它所做的只是包含一个头文件并尝试编译它。如果编译失败,则头文件本身缺少包含。

你可以通过以下规则获得相同的效果:foo.c 或 foo.cpp 必须包含的第一个头文件应该是相应命名的 foo.h。 这样做可以确保 foo.h 包含它需要编译的任何内容。

此外,Lakos 的Large-Scale C++ Software Design 一书(例如)列出了许多将实现细节从标头移出并移入相应 CPP 文件的技术。如果您将其发挥到极致,使用 Cheshire Cat(隐藏所有实现细节)和 Factory(隐藏子类的存在)之类的技术,那么许多标头将能够在不包含其他标头的情况下独立存在,而只需将声明转发到不透明类型...可能除了模板类。

最后,每个头文件可能需要包含:

  • 作为数据成员的类型没有头文件(相反,数据成员使用“柴郡猫”又名“pimpl”技术在 CPP 文件中定义/隐藏)

  • 对于作为方法参数或从方法返回类型的类型没有头文件(相反,这些类型是预定义的类型,例如 int;或者,如果它们是用户定义的类型,那么它们是其中的引用在这种情况下,前向声明的、不透明的类型声明(如仅在头文件中使用 class Foo; 而不是 #include "foo.h" 就足够了)。

那么你需要的是头文件:

  • 超类,如果这是子类

  • 可能是任何用作方法参数和/或返回类型的模板化类型:显然您也应该能够前向声明模板类,但某些编译器实现可能对此有问题(尽管您还可以将任何模板(例如List<X>)封装为用户定义类型(例如ListX)的实现细节。

在实践中,我可能会创建一个“standard.h”,其中包含任何/所有使用的所有系统文件(例如 STL 头文件、O/S 特定类型和/或任何#defines 等)项目中的头文件,并将其作为每个应用程序头文件中的第一个头文件包含在内(并告诉编译器将此“standard.h”视为“预编译头文件”)。


//contents of foo.h
#ifndef INC_FOO_H //or #pragma once
#define INC_FOO_H

#include "standard.h"
class Foo
{
public: //methods
  ... Foo-specific methods here ...
private: //data
  struct Impl;
  Impl* m_impl;
};
#endif//INC_FOO_H

//contents of foo.cpp
#include "foo.h"
#include "bar.h"
Foo::Foo()
{
  m_impl = new Impl();
}
struct Foo::Impl
{
  Bar m_bar;
  ... etc ...
};
... etc ...

【讨论】:

  • 现在我想起来了,你说得对。很酷的事情是,我多年来一直在这样做,而没有真正考虑过为什么。当我无知地做正确的事情时,我喜欢它。
  • 这是正确的。但是,如果您可以摆脱前向声明,那么只包含头文件是不正确的。因此,对于任何返回值、参数和指针类型,不应包含这些类型的标头,即使如果不向前声明类型,编译将失败。
  • 这是个中肯的建议,美国宇航局戈达德太空飞行中心在其 C 和 C++ 编码标准中进行了编纂。
  • 请注意,如果您在实现之间移动,那么您可能会发现不同的标准标头包含(或不包含)您的项目实际上需要的标头。这很讨厌,但受到 C++ 标准的认可,因为任何标准头文件都可能包含其他头文件。
  • 您可以前向声明模板类,因为您不能前向声明标准库的模板类,因为这是未定义的行为。
【解决方案2】:

我习惯将我的包含从高抽象级别排序到低抽象级别。这要求标头必须是自给自足的,并且隐藏的依赖项会作为编译器错误迅速显示出来。

例如,“俄罗斯方块”类有一个俄罗斯方块.h 和俄罗斯方块.cpp 文件。 Tetris.cpp 的包含顺序为

#include "Tetris.h"     // corresponding header first
#include "Block.h"      // ..then application level includes
#include "Utils/Grid.h" // ..then library dependencies
#include <vector>       // ..then stl
#include <windows.h>    // ..then system includes

现在我意识到这并不能真正回答您的问题,因为该系统并不能真正帮助清理不需要的包含。嗯嗯。。

【讨论】:

    【解决方案3】:

    根据您的项目大小,查看由doxygen(打开INCLUDE_GRAPH 选项)创建的包含图可能会有所帮助。

    【讨论】:

      【解决方案4】:

      this question 已经讨论了检测多余的包含。

      我不知道有任何工具可以帮助检测不足但发生在工作中的包含,但良好的编码约定可以在这里提供帮助。例如,Google C++ Style Guide 要求以下内容,目的是减少隐藏的依赖关系:

      dir/foo.cc 中,其主要目的是实现或测试dir2/foo2.h 中的内容,请按如下顺序排列您的包含:

      1. dir2/foo2.h(首选位置 - 详情见下文)。
      2. C 系统文件。
      3. C++ 系统文件。
      4. 其他库的 .h 文件。
      5. 您项目的 .h 文件。

      【讨论】:

        【解决方案5】:

        删除标头并重新编译技术的一个大问题是它可能导致仍在编译但错误或效率低下的代码。

          1234563 /p>
        1. 重载解决:一个类似的问题 - 如果您在不同的标头中有一个函数的两个重载,但采用某种兼容的类型,您最终可以删除更适合一种情况的版本,但仍然编译代码。这可能比模板专业化版本的可能性小,但有可能。

        【讨论】:

        • 3.代码编译错过了该文件中定义的函数的声明,这最初是可以的,但意味着函数声明和实际函数可能会在没有人注意到的情况下变得不同步。我使用 remove & recompile 技术遇到了这个问题 - 但发现我可以使用 sparse (gcc wrapper) 来检测这些情况。
        【解决方案6】:

        我一直在考虑写作 编译每个的东西 非头文件单独很多 次,每次删除#include 陈述。继续这样做,直到 实现了最少的包含集。

        我认为这是被误导的,并且会导致“不足但恰好起作用”的包含集。

        假设您的源文件使用numeric_limits,但还包含一些头文件,由于其自​​身的原因包含&lt;limits&gt;。这并不意味着您的源文件不应包含&lt;limits&gt;。可能没有记录其他头文件来定义&lt;limits&gt; 中定义的所有内容,它只是碰巧这样做了。有一天它可能会停止:也许它只使用一个值作为某个函数的默认参数,并且可能该默认值从 std::numeric_limits&lt;T&gt;::min() 更改为 0。现在您的源文件不再编译,并且它的维护者头文件甚至不知道你的文件存在,直到它破坏了他的构建。

        除非您此刻遇到严重的构建问题,否则我认为删除冗余包含的最佳方法就是养成每当您触摸文件进行维护时查看列表的习惯。如果您发现自己有几十个包含,并且在查看文件后仍然无法弄清楚每个包含的用途,请考虑将其分解为更小的文件。

        【讨论】:

        • 这尤其适用于系统头文件。 FWIW我可能会制作一个“standard.h”,其中包括项目中任何文件使用的所有系统文件(例如 STL 头文件),并将其作为每个应用程序头文件中的第一个头文件包含在内(并告诉编译器处理这个“ standard.h”作为“预编译的头文件”)。
        • 是的,我猜对于系统标头,您不会太在意包含不必要的内容。它可以被预编译,并且在任何情况下您都不会更改它们,因此您不会引发不必要的重建。当然,在大型项目中,拥有任何包含所有用户标头的“末日标头”是一个真的坏主意。
        【解决方案7】:

        如果您使用 Visual Studio 编译器,您可以尝试 /showIncludes 编译器选项,然后解析它向 stderr 发出的内容。 MSDN: "导致编译器输出包含文件的列表。嵌套的包含文件也会显示(包含在您包含的文件中的文件)。"

        【讨论】:

          【解决方案8】:

          是的。我们有自己的预处理器,可以让我们访问自己的宏语言。它还检查头文件是否只包含一次。创建一个简单的预处理器检查多个包含应该相当容易。

          【讨论】:

          • 预处理器不会限制您只包含一次头文件。事实上,一个正常人制作的每个头文件都被
             #ifndef MY_HEADER_H #define MY_HEADER_H //header code #endif 
            包围
          • 好吧,我想 OP 并没有真正询问多个包含,但问题是“如何在 C 或 C++ 项目中维护#include 语句?[...] 你创建了有什么工具可以发现或纠正问题?有什么建议吗?我不知道为什么这被否决了
          • 我使用过一次#pragma,但我认为在大多数现代编译器上几乎是一样的。我更喜欢它,因为它比尝试更少打字,更不容易出错。
          【解决方案9】:

          就工具而言,我在 Windows 上使用 Imagix(大约 6 年前)来识别不需要的包含以及需要但通过另一个包含间接包含的包含。

          【讨论】:

            【解决方案10】:

            看看cppclean 项目。虽然他们还没有实现该功能,但计划完成。

            来自项目网站:

            CppClean 尝试在 C++ 源代码中发现开发缓慢的问题 特别是在大型代码库中。它类似于 lint;然而, CppClean 专注于发现全局模块间问题,而不是 类似于其他静态分析工具的局部问题。

            目标是发现在大型代码库中拖慢开发的问题 随着时间的推移而修改,留下未使用的代码。这个代码可以进来 从未使用的函数、方法、数据成员、类型等多种形式到 不必要的#include 指令。不必要的#includes 可能导致 相当多的额外编译增加了编辑-编译-运行周期。

            尤其是#include 功能:

            • (计划中)查找不必要的头文件#included
              • 没有直接引用标题中的任何内容
              • 如果类是前向声明的,则不需要标题
            • (计划中)不直接#included 引用头文件的源文件,即依赖来自另一个头文件的传递#include 的文件

            Here你可以在BitBucket找到镜像。

            【讨论】:

              【解决方案11】:

              如果您在 Eclipse 中使用 CDT 进行编码,您可以使用 Organize Includes 命令。只需按 Ctrl+Shift+O,它就会添加必要的包含并删除不需要的。

              【讨论】:

                【解决方案12】:

                我通常会创建一个源文件(例如 main.c)和一个用于该源文件的头文件(main.h)。在源文件中,我将我在该文件中使用的所有主要类型的“接口”函数(在 main 中,它将是 main()),然后是重构这些函数后得到的任何函数(实现细节),往下走。在头文件中,我声明了一些 extern 函数,这些函数在其他源文件中定义,但在使用该头文件的源文件中使用。然后我声明我在该源文件中使用的任何结构或其他数据类型。

                然后我将它们全部编译并链接在一起。它保持干净整洁。一个典型的 include ... 部分,在我当前的项目中看起来像这样

                #include<windows.h>
                #include<windowsx.h>
                #include<stdio.h>
                
                #include"interface.h"
                #include"thissourcefile.h"
                
                //function prototypes
                
                //source
                

                有一个 interface 标头跟踪我在项目中所有表单中使用的数据结构,然后是 thissourcefile.h,它完全符合我刚才的解释(声明 externs 等)。

                另外,我从来没有在我的标题中定义任何东西,我只在那里放置声明。这样,它们可以被不同的源文件包含,并且仍然可以成功链接。函数原型(extern、static 或其他)和声明放在头文件中,这样它们可以被多次使用——定义放在源代码中,因为它们只需要在一个地方。

                如果您正在创建一个库或类似的东西,这显然会有所不同。但仅对于内部项目链接,我发现这可以保持一切都干净整洁。另外,如果你编写一个 makefile,(或者你只是使用一个 IDE),那么编译真的很简单和高效。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2014-07-11
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-06-06
                  • 1970-01-01
                  相关资源
                  最近更新 更多