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

3

当使用头文件时,每个头文件只应该被包含一次。
例如,假设我有三个类: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++编译器会抛出错误。
我的问题是,为什么我们需要指定包含保护呢?每个头文件只应该被包含一次不是常识吗?为什么编译器不自己处理多次包含的问题?

3
有时候确实需要多次包含同一个头文件。 - Benjamin Lindley
2
你能举个例子吗? - Rohit Shinde
我认为这是因为#include <header.h>的内容首先由预处理器处理,它基本上将头文件的内容放入主文件中,因此编译器看到的是大量重复的代码。 - rendon
1
这是一个关于为何多次#include同一个文件有用的SO帖子。https://dev59.com/j2gv5IYBdhLWcg3wDsnZ - R Sahu
3个回答

3
有时候,确实需要多次调用一个包含文件;在许多情况下,这是可取的。其中一个例子是优化大型复杂模板的实例化。考虑一些典型的大型复杂模板类。
template<typename T> class ComplicatedTemplate {

// ... Boring stuff goes here

};

拥有这个庞大的模板在每个翻译单元中被实例化和编译,对于同一模板类型,反复进行,变得非常老旧。它会减慢编译速度,并不必要地膨胀每个对象模块,只是让链接器处理剥离出大量重复的模板实例化。这是很多浪费的工作。
许多编译器提供控制模板实例化的方法。具体细节可能有所不同,但我将使用典型方法,如gcc所使用的方法,您可以在此处阅读:

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

假设您想要在名为 "complicated.cpp" 的翻译单元中实例化 ComplicatedTemplate<std::vector<int>>ComplicatedTemplate<std::vector<char>>ComplicatedTemplate<std::string<std::string>>,只需在头文件中声明为 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 文件来进行预实例化。实际的共享库还将第二个文件再次包含进去,在拉取外部可见头文件之后,通过相应的预处理器配置将那些外部模板声明转换为模板实例化。


3
我的问题是,为什么需要指定包含保护?每个头文件只被包含一次不是常识吗?为什么编译器不能自己处理多重包含的情况呢?
因为并非所有的头文件都符合这个规则。一个可以被包含多次且必须这样做的头文件的例子是<assert>头文件。
没有真正意义上的修复复制和粘贴文件内容的头文件系统的必要。我们真正需要的是转向更好的构建模型

0

我们通常认为编译是一个单一的步骤,但实际上涉及到各种步骤,在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;
}

而这段代码是编译器所处理的。此时,编译器无法提供太多帮助。

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


这回答了“什么”,但并没有真正回答“为什么”。 - Jack
这里提出的问题是为什么编译器不会自动添加保护措施。你回答了一个完全不同的问题——为什么需要它们,但这不是被问到的问题。 - Sam Varshavchik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接