标题包含C++中的蠕变

Header Include Creep in C++

本文关键字:包含 C++ 标题      更新时间:2023-10-16

当我开始学习C++时,我了解到头文件通常应该包含在其他头文件中。刚才有人告诉我,我应该在.cpp文件中包含一个特定的头文件,以避免头包含蠕变。

有人能告诉我这到底是什么,为什么会有问题吗?也许可以给我一些文档,告诉我什么时候我想在另一个头中包含一个文件,什么时候我应该在.cpp文件中包括一个文件?

"蠕变"是指包含一个标头,包括许多其他标头。这会带来一些不必要的后果:

  • 倾向于每个源文件都间接地包含每个标头,因此对任何标头的更改都需要重新编译所有内容
  • 循环依赖导致心痛的可能性更大

通过只声明所需的类,而不是包含给出完整定义的头,通常可以避免从另一个头中包含一个头。这样的不完全类型可以以各种方式使用:

// Don't need this
//#include "thingy.h"
// just this
class thingy;
class whatsit {
    // You can declare pointers and references
    thingy * p;
    thingy & r;
    // You can declare static member variables (and external globals)
    // They will need the header in the source file that defines them
    static thingy s;
    // You can declare (but not define) functions with incomplete
    // parameter or return types
    thingy f(thingy);
};

有些事情确实需要完整的定义:

// Do need this
#include "thingy.h"
// Needed for inheritance
class whatsit : thingy {
    // Needed for non-static member variables
    thingy t;
    // Needed to do many things like copying, accessing members, etc
    thingy f() {return t;}
};

有人能告诉我到底是什么吗

这不是一个编程术语,但在英语上下文中解释它意味着它是不必要的#include语句的引入。

为什么有问题

因为编译代码需要时间。所以编译不必要的代码需要不必要的时间。

也许可以给我一些文档,告诉我什么时候我想在另一个头中包含一个文件,什么时候我应该在.cpp文件中包括一个文件?

如果您的标头需要类型的定义,则需要包含定义该类型的标头。

#include "type.h"
struct TypeHolder
{
    Type t;
//  ^^^^ this value type requires the definition to know its size.
}

如果您的标头只需要类型的声明,则该定义是不必要的,include也是不必要的。

class Type;     
struct TypeHolder
{
    Type * t;
//  ^^^^^^ this pointer type has the already-known size of a pointer,
//         so the definition is not required.
}

作为一则支持这种做法价值的轶事,我曾经参与过一个项目,该项目的代码库需要一个小时才能完全编译。而更改一个头通常会花费下一次编译的大部分或全部时间

在适用的情况下,将前向声明添加到标头中,而不是立即包含,将完整编译时间减少到16分钟,并且更改标头通常不需要完全重建

嗯,他们可能指的是一些东西("包括蠕变"不是我以前听过的术语)。如果作为极端规则,包含来自标头的标头(除了每个源文件一个匹配的标头):

  • 编译时间会增加,因为必须为所有源文件编译许多标头
  • 不必要的依赖性增加,例如,在基于依赖性的构建系统中,修改一个标头可能会导致不必要的许多其他文件的重新编译,从而增加编译时间
  • 命名空间污染的可能性增加
  • 循环依赖关系成为一个问题(两个标头需要相互包含)
  • 其他更高级的依赖关系相关主题(例如,这违背了不透明指针的目的,后者在某些类型的应用程序中会导致许多设计问题)

根据一般经验,包含一个标头中仅编译该标头中的代码所需的最少数量(即,如果标头本身包含,则足以允许其进行编译,但不超过此数量)。这将解决上述所有问题。

例如,如果代码中有一个使用std::list的类的源文件,但该类本身没有使用std::list的成员或函数参数,则没有理由从该类的标头中使用#include <list>,因为该类标头中没有任何内容使用std::list。从您实际需要的源文件中执行,而不是在标头中执行,在标头中,使用类的所有内容都必须编译<list>

另一种常见的技术是正向声明指针类型。这是解决循环依赖问题的唯一真正方法,并且具有上面列出的一些编译时间优势。例如,如果两个类相互引用,但仅通过指针:

// A.h
class B; // forward declaration instead of #include "B.h"
class A {
   B *b;
}
// B.h
class A; // forward declaration instead of #include "A.h"
class B {
   A *a;
}

然后在文件中包含每个文件的头,其中实际需要定义。

您的头文件应包括单独(或首先)包含在源文件中时需要编译的所有头文件。在许多情况下,您可能允许通过使用前向声明来编译标头,但在包含您的标头之前,您不应该依赖于包含特定标头的人。

有很多时间需要考虑,但也有依赖性。如果A.h包含B.h和viceversa,代码将不会编译。你可以通过向前引用你的类,然后在cpp-中包括标题来解决这个问题

A.h
class B;
class A
{
    public:
    void CreateNewB():
    private:
    B* m_b;
};
A.cpp
#include "B.h"
A::CreateNewB()
{
   m_b = new B;
}