显式直接#include与非合同传递#include

时间:2019-06-14 11:06:56

标签: c++ header include c++17

说我们有这个头文件:

MyClass.hpp

#pragma once
#include <vector>

class MyClass
{
public:
    MyClass(double);

    /* ... */

private:
    std::vector<double> internal_values;
};

现在,每当我们在其他hpp或cpp文件中使用#include "MyClass.hpp"时,尽管事实上我们并不需要#include <vector>,但实际上我们也std::vector。我之所以说不需要,是因为MyClass仅在#pragma once #include "MyClass.hpp" void func(const MyClass&, const std::vector<double>&); 内部使用,但实际上完全不需要与该类进行交互。

结果,我可以写

版本1:SomeOtherHeader.hpp

#pragma once
#include "MyClass.hpp"
#include <vector>

void func(const MyClass&, const std::vector<double>&);

我可能应该

版本2:SomeOtherHeader.hpp

MyClass

防止依赖于MyClass的内部工作原理。还是应该?

我显然意识到<vector>需要#include才能工作。因此,这可能更多是一个哲学问题。但是,能够确定在导入时公开哪些标头(即限制将哪些标头加载到名称空间中)会不好吗?这样,每个标头都需要{{1}}需要什么 ,而不会隐含地包​​括链中需要的另一个标头呢?

也许人们也可以对即将到来的C ++ 20模块有所了解,我相信它可以解决此问题的某些方面。

6 个答案:

答案 0 :(得分:16)

  

防止依赖MyClass的内部工作原理。还是应该?

是的,出于这个原因,您应该这样做。除非您要确保MyClass.hpp保证包含<vector>,否则您不能依赖其中的一个。并且没有充分的理由被迫提供这种保证。如果没有这样的保证,那么您将依赖MyClass.hpp的实现细节,该细节将来可能会更改,这将破坏您的代码。

  

我显然意识到MyClass需要向量才能起作用。

是吗?它不能使用例如boost::container::small_vector吗?

  

在此示例中,MyClass需要std :: vector

但是,将来MyClass的需求又如何呢?程序不断发展,今天一堂课的需求与明天一堂课的需求并不总是相同的。

  

但是能够决定在导入时公开哪些标头不是很好

无法防止传递包含。

C ++ 20中引入的模块是一项功能,可以代替pp包含在内使用,目的是帮助解决此问题。

现在,您可以通过使用PIMPL模式(“实现的指针”)来避免包括任何实现细节依赖项。但是PIMPL引入了一个间接层,更重要的是,它需要动态分配,这会影响性能。根据具体情况,这些影响可以忽略不计或很重要。

答案 1 :(得分:7)

您应该使用显式#include来具有非破坏性的工作流程。假设MyClass用于50个不同的源文件中。它们不包括vector。突然,您必须为其他容器更改std::vector中的MyClass.h。然后,所有50个源文件要么需要包含vector,要么需要将其保留在MyClass.h中。  这将是多余的,并且可能不必要地增加应用程序大小编译时间甚至运行时间(静态变量初始化)。

答案 2 :(得分:3)

请考虑代码不仅要编写一次,而且会随着时间的推移而发展。

让我们假设您编写了代码,现在我的任务是重构代码。由于某种原因,我想用MyClass替换YourClass并假设它们具有相同的接口。我只需要用MyClass替换出现的YourClass即可:

/* Version 1: SomeOtherHeader.hpp */

#pragma once
#include "YourClass.hpp"

void func(const YourClass& a, const std::vector<double>& b);

我所做的一切都正确,但是代码仍然无法编译(因为YourClass不包含std::vector)。在这个特定的示例中,我将得到明确的错误消息,并且解决方法很明显。但是,如果这样的依赖关系跨越多个标头,如果有很多这样的依赖关系并且SomeOtherHeader.hpp包含的内容不仅仅是一个声明,那么事情就会变得相当混乱。

还有更多可能出错的地方。例如,MyClass的作者可以决定,他们实际上可以放弃包含,而使用向前声明。同样,SomeOtherHeader也将中断。可以归结为:如果您没有在vector中包含SomeOtherHeader,则存在一个隐藏的依赖关系,这很不好。

防止此类问题的经验法则是:包括您使用的内容。

答案 3 :(得分:3)

如果您的MyClass具有类型std::vector<double>的成员,则定义MyClass的标头需要#include <vector>。否则,MyClass的用户唯一的编译方式是如果他们在包含#include <vector>的定义之前MyClass

尽管成员是private,但它仍然是类的一部分,因此编译器需要查看完整的类型定义。否则,它将无法执行诸如计算sizeof(MyClass)或实例化任何MyClass对象的操作。

如果您想打破标头和<vector>之间的依赖关系,可以使用一些技巧。例如,pimpl(“实现的指针”)惯用法。

class MyClass 
{
public:
    MyClass(double first_value);

    /* ... */

private:
    void *pimpl;
};

,并且在定义该类成员的源文件中;

#include <vector>
#include "MyClass.hpp"

MyClass::MyClass(double first_value) : pimpl(new std::vector<double>())
{

}

(并且大概也可以对first_value进行操作,但是我已经省略了)。

需要权衡的是,每个需要使用向量的成员函数都需要从pimpl中获取它。例如,如果您想获得对已分配向量的引用

void MyClass::some_member_function()
{
    std::vector<double> &internal_data = *static_cast<std::vector<double> *>(pimpl);

}

MyClass的析构函数还需要释放动态分配的向量。

这也限制了类定义的某些选项。例如,MyClass不能具有按值返回std::vector<double>的成员函数(除非您#include <vector>

您需要确定像pimpl惯用语这样的技术是否值得让您的课堂发挥作用。就个人而言,除非有其他令人信服的理由使用pimpl习惯用法将类实现与类分开,否则我将简单地接受在头文件中需要#include <vector>

答案 4 :(得分:1)

是的,正在使用的文件应该明确包含<vector>,因为这是它需要的依赖项。

但是,我不会担心。如果有人重构MyClass.hpp以删除<vector> include,则编译器将依靠隐式include将它们指向缺少显式<vector> include的每个文件。修复此类错误通常很容易,一旦代码再次编译,一些缺失的显式包含项将得到修复。

最后,编译器在发现丢失的包含物方面比任何人都更有效率。

答案 5 :(得分:0)

正如其他人所说,直接保护您使用的文件是更安全的,这样可以防止将来对您要转发的文件进行更改。

通常也可以立即将您的依赖项放到那里,这一点更干净。如果要检查“ MyClass”对象是什么,则只需滚动到顶部,并要求您的IDE带您到相关的标题。

值得注意的是,安全地多次包含相同的标准标头,这是标准库保证所提供的。实际上,这意味着(in say clang's libc++)的实现将从#include保护开始。现代编译器非常熟悉include防护习惯用法(尤其是由其自己的标准库实现应用的习惯),以至于他们甚至可以避免加载文件。因此,要换取安全性和清晰度,您唯一要失去的就是必须多输入十几个字母。

所有与其他人都同意的内容,我已经重新阅读,并且我不认为您的问题实际上是“我应该这样做吗?”就像“为什么我什至不允许这样做?”或“为什么编译器不使我与我的包含”包含隔离?

“直接包含您使用的内容”规则有一个重要的例外。这是标头,作为其规范的一部分,其中包括其他标头。例如,从c ++ 11开始,保证<iostream>(当然它本身也是标准库的一部分)包括<istream><ostream>。有人可能会说:“为什么不仅将<istream><ostream>的内容直接移到<iostream>中?”但是,如果只需要一个,可以选择将它们拆分的方法具有清晰性和编译速度优势。 (而且,毫无疑问,对于c ++,也是有历史原因的。)您当然也可以为自己的标头执行此操作。 (更多的是Objective-C的东西,但是它们具有相同的包含机制,并且通常将它们用于伞头,其唯一的工作就是包含其他文件。)

还有另一个根本原因,就是包含的标头包含被包含在内。就是说,通常来说,没有标题就没有标题。假设您的MyClass.hpp文件包含以下类型同义词

using NumberPack = std::vector<unsigned int>;

和以下自我描述功能

NumberPack getFirstTenNumers();

现在假设另一个文件包含MyClass.hpp并具有以下内容。

NumberPack counter = getFirstTenNumbers();
for (auto c : counter) {
    std::cout << c << "\n"
}

这里发生的事情是您可能不想将正在使用<vector>的代码写进去。这是您不需要担心的实现细节。就您而言,NumberPack可以实现为其他一些容器或迭代器或生成器类型的东西,只要符合其规范即可。但是编译器需要知道它的实际含义:在不知道祖父母依赖项标头是什么的情况下,它不能有效利用父依赖项。这样做的副作用是您不必使用它们。

或者,当然,第三个原因仅仅是“因为那不是C ++”。是的,可能有一种语言没有传承第二代依赖关系,或者您必须明确要求它。只是它将是另一种语言,特别是不适合基于c ++或老友记的旧文本包含样式。