包含保护:为什么C++编译器不自动只包含每个头文件一次?

Include Guards: Why doesn't the C++ compiler automatically include each header file once only?

本文关键字:文件 一次 包含每 为什么 保护 C++ 编译器 包含      更新时间:2023-10-16

使用头文件时,每个头文件只应包含一次。
例如,假设我有三个类。 class Aclass Bclass C

class A在文件 A.h 中声明,class B在文件 B.h 中声明,

class C在文件 C.h 中声明,它们在各自的 .cpp 文件中定义。
A.cpp

#include "A.h"
class A  
{  
}  

B.cpp文件中,以下内容将是类的定义。

#include "A.h"
#include "B.h"
class B  
{  
   A a;
}

C.cpp文件也是如此。

#include "A.h"
#include "B.h"
#include "C.h"
class C  
{  
  A a;  
  B b;  
}  

现在,如果包含保护未写入头文件中,则 g++ 编译器将引发错误。
我的问题是,为什么我们需要指定包含保护?每个头文件只应包含一次不是常识吗?为什么编译器不自己处理多个包含?

我的问题是,为什么我们需要指定包含保护?每个头文件只应包含一次不是常识吗?为什么编译器不自己处理多个包含?

因为并非所有标头都是如此。可以多次包含的标头的一个示例是 <assert> 标头,并且能够这样做实际上很重要。


尝试修复复制和粘贴文件内容的标头系统实际上没有任何意义。实际上,我们只需要转向更好的构建模型。

如前所述,有时您确实希望多次调用包含文件;并且在很多情况下,这可能是可取的。

这有用的一个例子是优化大型复杂模板的实例化。考虑一些典型的大型复杂模板类

template<typename T> class ComplicatedTemplate {
// ... Boring stuff goes here
};

在每个翻译单元中实例化和编译这个大模板,一遍又一遍地使用相同的模板类型,会变得非常陈旧。它减慢了编译速度,并且不必要地使每个对象模块膨胀,只是为了让链接器处理去除大量重复的模板实例化。这是很多浪费的工作。

许多编译器提供了控制模板实例化的方法。确切的细节有时会有所不同,但我将使用 gcc 使用的典型方法,您可以在此处阅读:

https://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html

比如说,你想在实例化ComplicatedTemplate<std::vector<int>>时消耗一些CPU时间,ComplicatedTemplate<std::vector<char>>,也许ComplicatedTemplate<std::string<std::string>>在一个名为"complex.cpp"的翻译单元中,并在头文件中声明它们extern。

好的,所以你最终会得到这个complicated_template.H

template<typename T> class ComplicatedTemplate {
// ... Boring stuff goes here
};
extern template ComplicatedTemplate<std::vector<int>>;
extern template ComplicatedTemplate<std::vector<char>>;
extern template ComplicatedTemplate<std::vector<std::string>>;

然后,在complicated.cpp

#include "complicated_template.H"
template ComplicatedTemplate<std::vector<int>>;
template ComplicatedTemplate<std::vector<char>>;
template ComplicatedTemplate<std::vector<std::string>>;

好的,所以这将正常工作,除了一个不便。如果您决定将ComplicatedTemplate<std::vector<SomeCustomType>>或其他任何内容添加到预实例化模板列表中,则需要在两个位置完成此操作;在头文件中和complicated.cpp

以下是消除这种重复的典型方法:

complicated_template.H

template<typename T> class ComplicatedTemplate {
// ... Boring stuff goes here
};
#include "complicated_template_inst.H"

complicated_template_inst.H

#ifndef EXTERN
#define EXTERN
#endif
EXTERN template ComplicatedTemplate<std::vector<int>>;
EXTERN template ComplicatedTemplate<std::vector<char>>;
EXTERN template ComplicatedTemplate<std::vector<std::string>>;

然后,在complicated.cpp

#include "complicated_template.H"
#define EXTERN
#include "complicated_template_inst.H"

现在,预实例化的模板实例列表位于一个位置。使用前面的示例,添加:

EXTERN template ComplicatedTemplate<std::vector<SomeCustomType>>;

具有防止在需要该模板实例的每个翻译单元中浪费此模板实例化的效果,以及在complicated.cpp翻译单元中显式实例化它的效果。

您将在许多大型C++库中看到这种方法。他们通常会定义他们的模板,然后通过拉入一个单独的 #include 文件来预先实例化它们,该文件包含一些预处理器-fu。在拉入外部可见的头文件之后,实际的共享库还将第二次包含第二个文件,预处理器相应地进行装配,以将这些 extern 模板声明转换为模板实例化。

我们通常认为编译是一个步骤,但实际上涉及各种步骤,在C++的情况下,其中一个阶段是预处理,它处理#include <header.h>的东西,它基本上将每个头文件的内容(以及类似 #define (放在你的主文件中,所以,如果你不制定适当的条件,你的主源文件最终会得到重复的代码。

例如,假设您有两个文件:

// a.h
class A {
};

// b.cpp
#include "a.h"
#include "a.h"
int main()
{
    return 0;
}

在实际编译之前,预处理器将处理b.cpp,预处理器使用g++结果如下所示:

# 1 "b.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "b.cpp"
# 1 "a.h" 1
class A {
};
# 2 "b.cpp" 2
# 1 "a.h" 1
class A {           // Repeated code
};
# 3 "b.cpp" 2
int main()
{
    return 0;
}

最后这段代码是编译器所处理的。在这一点上,编译器帮不上太多忙。

这是一个非常简单的例子,但我认为它可以帮助你。