如何在C或C ++项目中维护#include语句?似乎几乎不可避免的是,最终文件中的包含语句集合不足(但由于项目的当前状态而恰好起作用)或者包含不再需要的东西。
您是否创建了任何工具来发现或纠正问题?有什么建议吗?
我一直在考虑编写一些可以单独编译每个非头文件的东西,每次删除#include语句。继续这样做,直到达到最小的包含。
要验证头文件是否包含他们需要的所有内容,我会创建一个源文件,它所做的只是包含头文件并尝试编译它。如果编译失败,那么头文件本身就缺少一个include。
在我创作之前,我想我应该在这里问一下。这似乎是一个普遍的问题。
答案 0 :(得分:23)
为了验证头文件是否包含他们需要的所有内容,我将创建一个源文件,它所做的只是包含头文件并尝试编译它。如果编译失败,那么头文件本身就缺少一个include。
通过制定以下规则可以获得相同的效果: foo .c或 foo .cpp必须包含的第一个头文件应该是相应命名的< EM> FOO 的.h。 这样做可以确保 foo .h包含编译所需的任何内容。
此外,Lakos的书籍大型C ++软件设计(例如)列出了许多用于将实现细节从标题移动到相应CPP文件中的技术。如果你把它发挥到极致,使用Cheshire Cat(隐藏所有实现细节)和Factory(隐藏子类的存在)等技术,那么许多标题将能够独立而不包括其他标题,而是使用将声明转发给opaque类型而不是......除了模板类之外。
最后,每个头文件可能需要包括:
没有作为数据成员的类型的头文件(而是使用“cheshire cat”a.k.a.“pimpl”技术在CPP文件中定义/隐藏数据成员)
对于作为参数或从方法返回类型的类型没有头文件(相反,这些是预定义的类型,如int
;或者,如果它们是用户定义的类型,那么它们是引用在这种情况下,头文件中只有class Foo;
而不是#include "foo.h"
的前向声明的opaque类型声明就足够了。
您需要的是:
的头文件超类,如果这是一个子类
可能是用作方法参数和/或返回类型的任何模板化类型:显然你应该能够前向声明模板类,但是某些编译器实现可能有问题(尽管您还可以封装任何模板,例如List<X>
作为用户定义类型的实现细节,例如ListX
)。
实际上,我可能会创建一个“standard.h”,其中包含任何系统文件(例如STL标头,O / S特定类型和/或任何#define
等)。 /项目中的所有头文件,并将其作为每个应用程序头文件中的第一个头文件(并告诉编译器将此“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 ...
答案 1 :(得分:11)
我习惯将我的包括从高抽象级别排序到低抽象级别。这要求标头必须是自给自足的,并且隐藏的依赖关系会很快显示为编译器错误。
例如,“俄罗斯方块”类有一个Tetris.h和Tetris.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
现在我意识到这并没有真正回答你的问题,因为这个系统并没有真正帮助清理不需要的包含。好吧..
答案 2 :(得分:6)
已在this question中讨论了检测多余包含。
我不知道有任何工具可以帮助检测不足但发生在工作中的内容,但良好的编码约定可以在这里提供帮助。例如,Google C++ Style Guide强制执行以下操作,目标是减少隐藏的依赖项:
在dir/foo.cc
,其主要目的是在dir2/foo2.h
中实施或测试内容,请按以下方式订购您的内容:
dir2/foo2.h
(首选位置 - 请参阅下面的详细信息)。答案 3 :(得分:6)
根据项目的大小,查看doxygen创建的包含图表(启用INCLUDE_GRAPH
选项)可能会有所帮助。
答案 4 :(得分:5)
删除标头和重新编译技术的一个大问题是它可能导致仍然编译,但错误或低效的代码。
模板特化:如果您对一个标题中的特定类型具有模板特化,而另一个标题中具有更通用的模板,则删除特化可能会使代码处于可编译状态,但会产生不良结果。< / p>
重载解决方案:一个类似的问题 - 如果你在不同的标题中有一个函数的两个重载但是需要稍微兼容的类型,你可以最终删除一个更适合一个案例的版本,但仍然让代码编译。这可能不如模板专业化版本,但它是可能的。
答案 5 :(得分:2)
我一直在考虑写作 什么东西编译每个 非头文件分别很多 每次,每次删除#include 声明。继续这样做直到a 实现了最小的包含。
我认为这是误导的,并且会导致“不足但恰好正常工作”包括套装。
假设您的源文件使用numeric_limits
,但还包含一些头文件,由于其自身包含<limits>
。这并不意味着您的源文件不应包含<limits>
。可能没有记录其他头文件来定义<limits>
中定义的所有内容,它恰好也是如此。有一天它可能会停止:也许它只使用一个值作为某个函数的默认参数,也许默认值从std::numeric_limits<T>::min()
变为0.现在你的源文件不再编译了,维护者该头文件甚至不知道你的文件存在,直到它破坏了他的构建。
除非你在这一刻遇到严重的构建问题,否则我认为删除冗余包含的最佳方法就是养成在每次触摸文件进行维护时查看列表的习惯。如果你发现你有几十个包含,并且审查了文件,你仍然无法弄清楚每个包含的内容,请考虑分解成较小的文件。
答案 6 :(得分:2)
如果使用Visual Studio编译器,可以尝试/ showIncludes编译器选项,然后解析它向stderr发出的内容。 MSDN: “使编译器输出包含文件的列表。还会显示嵌套的包含文件(包含在您包含的文件中的文件)。”
答案 7 :(得分:1)
烨。我们有自己的预处理器,可以让我们访问自己的宏语言。它还会检查头文件是否只包含一次。创建一个简单的预处理器来检查多个包含应该相当容易。
答案 8 :(得分:1)
就工具而言,我已经在Windows上使用了Imagix(这是大约6年前)来识别不需要的包含以及包含哪些包含但通过其他包含间接包含的包含。
答案 9 :(得分:1)
看看cppclean项目。虽然他们尚未实现该功能,但计划完成。
从项目现场:
CppClean试图在C ++源代码中发现问题,从而减缓开发速度 特别是在大型代码库中。它类似于棉绒;然而, CppClean专注于寻找全局模块间问题而不是 本地问题与其他静态分析工具类似。
目标是发现在大型代码库中减缓开发的问题 随着时间的推移修改,留下未使用的代码这段代码可以进来 从未使用的函数,方法,数据成员,类型等的许多形式 不必要的#include指令。不必要的#includes会导致 相当多的额外编译增加了编辑 - 编译 - 运行周期。
尤其是#include功能:
- (计划)查找不必要的头文件#included
- 没有直接引用标题中的任何内容
- 如果类是向前声明的话,则不需要标题
- (计划)不直接引用标题的源文件#included,即依赖于来自另一个标题的传递#include的文件
Here你可以在BitBucket找到一面镜子。
答案 10 :(得分:0)
如果您使用CDT在Eclipse中编码,则可以使用“组织包含”命令。只需按Ctrl + Shift + O即可添加必要的包含并删除不需要的包含。
答案 11 :(得分:-1)
我通常创建一个源文件(例如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),那么编译非常简单有效。