说我们有这个头文件:
#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>&);
内部使用,但实际上完全不需要与该类进行交互。
结果,我可以写
#pragma once
#include "MyClass.hpp"
#include <vector>
void func(const MyClass&, const std::vector<double>&);
我可能应该写
MyClass
防止依赖于MyClass
的内部工作原理。还是应该?
我显然意识到<vector>
需要#include
才能工作。因此,这可能更多是一个哲学问题。但是,能够确定在导入时公开哪些标头(即限制将哪些标头加载到名称空间中)会不好吗?这样,每个标头都需要{{1}}需要什么 ,而不会隐含地包括链中需要的另一个标头呢?
也许人们也可以对即将到来的C ++ 20模块有所了解,我相信它可以解决此问题的某些方面。
答案 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 ++或老友记的旧文本包含样式。