17 回答
你的同事错了,常见的方法是并且一直都是将代码放在 .cpp 文件(或任何你喜欢的扩展名)中,并在标题中声明。
有时将代码放在头文件中会有一些好处,这可以让编译器更聪明地进行内联。但与此同时,它会破坏您的编译时间,因为每次编译器包含所有代码时都必须对其进行处理。
最后,当所有代码都是标题时,循环对象关系(有时是需要的)通常很烦人。
归根结底,你是对的,他是错的。
编辑:我一直在考虑你的问题。有一种情况,他说的是真的。模板。许多较新的“现代”库(例如 boost)大量使用模板,并且通常是“仅标题”。但是,这只能在处理模板时进行,因为这是处理模板时的唯一方法。
编辑:有些人想要更清楚一点,这里有一些关于编写“仅标题”代码的缺点的想法:
如果你四处搜索,你会看到很多人在处理 boost 时试图找到一种减少编译时间的方法。例如:如何使用 Boost Asio 减少编译时间,它可以看到单个 1K 文件的 14 秒编译,其中包含 boost。14 秒似乎没有“爆炸式增长”,但它肯定比典型的要长得多,并且在处理大型项目时可以很快加起来。仅标头库确实以可衡量的方式影响编译时间。我们只是容忍它,因为 boost 非常有用。
此外,还有很多事情不能仅在标头中完成(即使 boost 具有您需要链接到某些部分的库,例如线程、文件系统等)。一个主要的示例是,您不能在仅标头库中拥有简单的全局对象(除非您求助于单例的可恶),因为您将遇到多个定义错误。注意: C++17 的内联变量将使这个特定示例在未来变得可行。
最后一点,当使用 boost 作为仅标头代码的示例时,经常会遗漏一个巨大的细节。
Boost 是库,而不是用户级代码。所以它不会经常改变。在用户代码中,如果你把所有东西都放在头文件中,每一个小改动都会导致你不得不重新编译整个项目。这是对时间的巨大浪费(对于不会从编译到编译的库来说,情况并非如此)。当您在标头/源代码和更好的情况下拆分内容时,使用前向声明来减少包含,您可以在一天内加起来节省数小时的重新编译时间。
在 C++ 程序员同意The Way的那一天,羔羊将与狮子一起躺下,巴勒斯坦人将拥抱以色列人,猫和狗将被允许结婚。
在这一点上,.h 和 .cpp 文件之间的分离主要是任意的,这是很久以前编译器优化的痕迹。在我看来,声明属于头文件,定义属于实现文件。但是,这只是习惯,不是宗教。
标头中的代码通常不是一个好主意,因为当您更改实际代码而不是声明时,它会强制重新编译包含标头的所有文件。它还会减慢编译速度,因为您需要解析每个包含标头的文件中的代码。
在头文件中包含代码的一个原因是关键字 inline 通常需要它才能正常工作以及使用在其他 cpp 文件中实例化的模板时。
可能会告诉您同事的是,大多数 C++ 代码都应该模板化以实现最大可用性。如果它是模板化的,那么所有内容都需要在头文件中,以便客户端代码可以看到它并实例化它。如果它对 Boost 和 STL 来说足够好,那对我们来说也足够好了。
我不同意这种观点,但这可能是它的来源。
我认为你的同事很聪明,你也很正确。
我发现将所有内容放入标题中的有用之处在于:
无需编写和同步标头和源。
结构很简单,没有循环依赖迫使编码器制作“更好”的结构。
便携,易于嵌入到新项目中。
我同意编译时间问题,但我认为我们应该注意到:
源文件的更改很可能会更改头文件,从而导致整个项目重新编译。
编译速度比以前快很多。而如果你有一个项目要建设的时间长、频率高,这可能表明你的项目设计存在缺陷。将任务分成不同的项目和模块可以避免这个问题。
最后,我只是想支持你的同事,就我个人而言。
通常我会将琐碎的成员函数放入头文件中,以允许它们被内联。但是要将整个代码体放在那里,只是为了与模板保持一致?这简直是疯了。
记住:愚蠢的一致性是小头脑的妖精。
正如 Tuomas 所说,您的标题应该是最小的。为了完整,我将扩展一点。
我个人在我的C++
项目中使用了 4 种类型的文件:
- 上市:
- 转发标头:在模板等情况下,此文件获取将出现在标头中的转发声明。
- 标头:此文件包括转发标头(如果有),并声明我希望公开的所有内容(并定义类...)
- 私人的:
- 私有头文件:这个文件是为实现而保留的头文件,它包括头文件并声明了辅助函数/结构(例如用于 Pimpl 或谓词)。如果不需要,请跳过。
- 源文件:它包括私有标头(或标头,如果没有私有标头)并定义所有内容(非模板...)
此外,我将此与另一条规则相结合:不要定义您可以转发声明的内容。虽然我在那里当然是合理的(在任何地方使用 Pimpl 都很麻烦)。
这意味着#include
只要我可以摆脱它们,我更喜欢前向声明而不是标题中的指令。
最后,我还使用了可见性规则:我尽可能地限制符号的范围,以免它们污染外部范围。
总而言之:
// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;
// example.hpp
#include "project/example_fwd.hpp"
// Those can't really be skipped
#include <string>
#include <vector>
#include "project/pimpl.hpp"
// Those can be forward declared easily
#include "project/foo_fwd.hpp"
namespace project { class Bar; }
namespace project
{
class MyClass
{
public:
struct Color // Limiting scope of enum
{
enum type { Red, Orange, Green };
};
typedef Color::type Color_t;
public:
MyClass(); // because of pimpl, I need to define the constructor
private:
struct Impl;
pimpl<Impl> mImpl; // I won't describe pimpl here :p
};
template <class T> class MyClassT: public MyClass {};
} // namespace project
// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"
template <class T> void check(MyClass<T> const& c) { }
// example.cpp
#include "example_impl.hpp"
// MyClass definition
这里的救星是大多数时候前向标头是无用的:只有在typedef
or的情况下才需要template
实现标头;)
为了增加乐趣,您可以添加.ipp
包含模板实现(包含在 中.hpp
)的文件,同时.hpp
包含接口。
除了模板化代码(取决于项目,这可能是大多数或少数文件)之外,还有普通代码,在这里最好将声明和定义分开。在需要的地方还提供前向声明——这可能会影响编译时间。
一般来说,写一个新类的时候,我会把所有的代码都放在这个类里,这样我就不用再找另一个文件了。一切正常后,我把方法体分解成cpp文件,将原型留在 hpp 文件中。
我个人在我的头文件中这样做:
// class-declaration
// inline-method-declarations
我不喜欢将方法的代码与类混合,因为我发现快速查找内容很痛苦。
我不会将所有方法都放在头文件中。编译器(通常)不能内联虚方法,并且(可能)只能内联没有循环的小方法(完全取决于编译器)。
在课堂上做这些方法是有效的......但从可读性的角度来看,我不喜欢它。将方法放在标题中确实意味着,如果可能,它们将被内联。
如果这种新方式真的是The Way,我们的项目可能会遇到不同的方向。
因为我们试图避免标题中所有不必要的东西。这包括避免标头级联。标头中的代码可能需要包含其他标头,这将需要另一个标头,依此类推。如果我们被迫使用模板,我们会尽量避免在标题中过多地使用模板。
此外,我们在适用时使用“不透明指针”模式。
通过这些实践,我们可以比大多数同行进行更快的构建。是的......更改代码或类成员不会导致大规模重建。
我认为将所有函数定义都放入头文件中绝对是荒谬的。为什么?因为头文件用作类的 PUBLIC 接口。这是“黑匣子”的外面。
当你需要查看一个类来参考如何使用它时,你应该查看头文件。头文件应该给出一个它可以做什么的列表(注释来描述如何使用每个函数的细节),它应该包括一个成员变量的列表。它不应该包括每个单独的功能是如何实现的,因为这是一堆不必要的信息,只会使头文件混乱。
我把所有的实现都放在了类定义之外。我想从类定义中删除 doxygen 注释。
恕我直言,他只有在做模板和/或元编程时才有价值。已经提到了很多将头文件限制为声明的原因。他们只是……标题。如果要包含代码,请将其编译为库并将其链接起来。
这真的不取决于系统的复杂性和内部约定吗?
目前,我正在研究一个非常复杂的神经网络模拟器,我期望使用的公认风格是:
classname.h 中的类定义 classnameCode.h中的
类代码 classname.cpp 中的
可执行代码
这将用户构建的模拟与开发人员构建的基类分开,并且在这种情况下效果最好。
但是,如果看到人们在图形应用程序或任何其他目的不是为用户提供代码库的应用程序中这样做,我会感到惊讶。
模板代码应该只在标题中。除此之外,除内联之外的所有定义都应在 .cpp 中。对此最好的论据是遵循相同规则的标准库实现。您不会不同意 std lib 开发人员对此是正确的。
我认为您的同事是对的,只要他不进入在标头中编写可执行代码的过程即可。我认为,正确的平衡是遵循 GNAT Ada 指示的路径,其中 .ads 文件为其用户及其子项提供了包的完全充分的接口定义。
顺便说一句,Ted,你有没有在这个论坛上看过最近关于 Ada 绑定到你几年前写的 CLIPS 库的问题,现在不再可用(相关网页现已关闭)。即使针对旧的 Clips 版本,此绑定对于愿意在 Ada 2012 程序中使用 CLIPS 推理引擎的人来说也是一个很好的开始示例。