将c++定义放在头文件中是一个好习惯吗?

我个人使用c++的风格总是将类声明放在包含文件中,并将定义放在.cpp文件中,非常像洛基的回答 c++头文件,代码分离中规定的那样。不可否认,我喜欢这种风格的部分原因可能与我花了这么多年来编写Modula-2和Ada有关,这两者都有类似的规范文件和主体文件方案。

我有一个同事,他比我更了解c++,他坚持认为所有c++声明都应该尽可能地在头文件中包含定义。他并不是说这是一种有效的替代风格,甚至不是一种稍微更好的风格,而是说这是一种新的普遍接受的风格,现在每个人都在使用c++。

我不像以前那么灵活了,所以在我看到更多的人和他在一起之前,我并不急于加入他的行列。那么这个习语到底有多普遍呢?

只是给答案一些结构:现在是™的方式,非常常见,有点常见,不常见,还是bug-out疯狂?

147471 次浏览

你的同事错了,通常的方法是把代码放在。cpp文件(或任何你喜欢的扩展名)中,并在头文件中声明。

将代码放在头文件中偶尔会有一些好处,这可以让编译器更聪明地进行内联。但与此同时,它会破坏编译时间,因为所有代码在每次被编译器包含时都必须被处理。

最后,当所有的代码都是头部时,循环对象关系(有时是需要的)通常是令人讨厌的。

总之,你是对的,他是错的。

我一直在思考你的问题。有一个的情况下,他说的是真的。模板。许多更新的“现代”;像boost这样的库大量使用模板,并且通常“只有头文件”。但是,只有在处理模板时才应该这样做,因为这是处理模板时的唯一方法。

编辑:有些人想要更多的澄清,这里有一些关于只写“标题”的缺点的想法。代码:

如果你四处搜索,你会发现很多人在处理boost时都试图找到一种减少编译时间的方法。例如:如何减少编译时间与Boost Asio,它看到一个包含boost的1K文件的14s编译。14秒可能看起来不是“爆炸”,但它肯定比一般情况下要长得多,而且在处理一个大项目时可以很快加起来。只有头文件的库确实会以相当可测量的方式影响编译时间。我们只是容忍它,因为boost太有用了。

此外,还有许多事情不能仅在头文件中完成(甚至boost也有一些库,您需要链接到某些部分,如线程、文件系统等)。一个主要的例子是你不能在只有头的库中有简单的全局对象(除非你求助于讨厌的单例),因为你会遇到多个定义错误。注意: c++ 17的内联变量将使这个特殊的例子在未来可行。

最后一点,当使用boost作为仅头代码的示例时,经常会忽略一个巨大的细节。

Boost是库,而不是用户级代码。所以它不会经常变化。在用户代码中,如果你把所有东西都放在头文件中,每一个小的改变都会导致你不得不重新编译整个项目。这是对时间的巨大浪费(对于那些在不同的编译器之间不进行更改的库来说,情况并非如此)。当你在头文件/源文件之间拆分时,更好的是,使用前向声明来减少包含,你可以在一天内节省数小时的重新编译时间。

我个人在头文件中这样做:

// class-declaration


// inline-method-declarations

我不喜欢将方法的代码与类混合在一起,因为我发现快速查找东西很痛苦。

我不会把所有的方法都放在头文件中。编译器(通常)不能内联虚拟方法,(可能)只内联没有循环的小方法(完全取决于编译器)。

在类中执行方法是有效的…但从可读性的角度来看,我不喜欢它。将方法放在头文件中确实意味着,如果可能的话,它们将被内联。

c++程序员同意的方式的那一天,羊羔将与狮子躺在一起,巴勒斯坦人将拥抱以色列人,猫和狗将被允许结婚。

在这一点上,.h和.cpp文件之间的分离基本上是任意的,这是编译器优化很久以前的遗留问题。在我看来,声明属于头文件,定义属于实现文件。但是,那只是习惯,不是宗教。

头文件中的代码通常是一个坏主意,因为当您更改实际代码而不是声明时,它会强制重新编译包含头文件的所有文件。它还会降低编译速度,因为您需要解析每个包含头的文件中的代码。

将代码放在头文件中的一个原因是,当使用其他cpp文件中实例化的模板时,关键字inline通常需要它才能正常工作。

如果这个新方法真的是的方式,我们可能在我们的项目中遇到了不同的方向。

因为我们尽量避免在头文件中出现不必要的东西。这包括避免头级联。头文件中的代码可能需要包含一些其他头文件,而这些头文件又需要另一个头文件,以此类推。如果我们被迫使用模板,我们尽量避免在标题中使用太多模板。

同样,我们在适用时使用“不透明的指针”模式

通过这些实践,我们可以比大多数同行更快地构建。是的……更改代码或类成员不会导致巨大的重构。

你的同事可能知道的是,大多数c++代码都应该被模板化,以实现最大的可用性。如果它是模板化的,那么一切都需要在一个头文件中,以便客户端代码可以看到它并实例化它。如果它对Boost和STL来说足够好,它对我们来说就足够好了。

我不同意这种观点,但这可能是它的来源。

为了增加更多的乐趣,你可以添加包含模板实现(包含在.hpp中)的.ipp文件,而.hpp包含接口。

除了模板化代码(取决于项目,这可以是大多数或少数文件),还有正常的代码,在这里最好将声明和定义分开。在需要的地方也提供前向声明——这可能会影响编译时间。

我把所有实现都放在类定义之外。我想把doxygen注释从类定义中删除。

我经常会把琐碎的成员函数放到头文件中,以允许它们内联。但是将整个代码体放在那里,只是为了与模板保持一致?那完全是胡说八道。

记住:愚蠢的一致性是小心眼的妖怪

通常,当编写一个新类时,我会把所有的代码放在类中,所以我不必在另一个文件中寻找它。在一切正常工作之后,我将方法的主体分解到cpp文件中,将原型留在hpp文件中。

恕我直言,他只有在做模板和/或元编程时才有价值。前面已经提到了将头文件限制为声明的很多原因。他们只是…头。如果您希望包含代码,则将其编译为库并将其链接起来。

这难道不是取决于系统的复杂性和内部约定吗?

目前,我正在研究一个非常复杂的神经网络模拟器,我期望使用的公认风格是:

classname.h中的类定义
.h classnameCode.h中的类代码
classname.cpp

.cpp中的可执行代码

这将用户构建的模拟从开发人员构建的基类中分离出来,在这种情况下效果最好。

但是,如果有人在图形应用程序或其他目的不是为用户提供代码库的应用程序中这样做,我会感到惊讶。

正如图马斯所说,你的头球应该是最小的。为了完整,我将展开一点。

我个人在我的C++项目中使用4种类型的文件:

  • 公众:
  • 转发头:在模板等的情况下,该文件获得将出现在头中的转发声明。
  • Header:这个文件包括转发头,如果有的话,并声明我希望公开的所有内容(并定义类…)
  • 私人:
  • 私有头文件:该文件是为实现保留的头文件,它包括头文件并声明了helper函数/结构(例如用于Pimpl或谓词)。如果没有必要,跳过。
  • 源文件:它包括私有头文件(如果没有私有头文件则是头文件)并定义了所有内容(非模板…)

此外,我还附带了另一条规则:不要定义可以转发声明的内容。当然,我在那里是合理的(到处使用皮impl是相当麻烦的)。

这意味着我更喜欢在头文件中使用前向声明而不是#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

这里的救星是大多数时候forward头是无用的:只有在typedeftemplate的情况下才需要,实现头也是;)

模板代码应该只在头文件中。除此之外,除了内联之外的所有定义都应该在.cpp中。最好的参数是遵循相同规则的std库实现。你不会不同意std lib开发人员在这方面是正确的。

我认为你的同事很聪明,你也是对的。

我发现把所有东西都放在头文件中的有用的事情是:

  1. 不需要写作&同步头文件和源文件。

  2. 结构很简单,没有循环依赖迫使编码器做出“更好”的结构。

  3. 便携,易于嵌入到新项目中。

我同意编译时间的问题,但我认为我们应该注意到:

  1. 源文件的改变很可能会改变头文件,从而导致整个项目重新编译。

  2. 编译速度比以前快多了。而如果你的项目建设时间长、频率高,这可能说明你的项目设计存在缺陷。将任务分离到不同的项目和模块中可以避免这个问题。

最后,我只是想支持你的同事,只是在我个人看来。

我认为你的同事是对的,只要他没有进入这个过程,在头部写可执行代码。 我认为,正确的平衡是遵循GNAT Ada所指示的路径,其中.ads文件为其用户和其子程序包提供了完全充分的接口定义

顺便说一句,Ted,你有没有在这个论坛上看到最近关于Ada绑定到CLIPS库的问题,这个问题是你几年前写的,现在已经没有了(相关的网页现在已经关闭了)。即使使用的是旧的Clips版本,对于愿意在Ada 2012程序中使用Clips推理引擎的人来说,这个绑定也是一个很好的开始示例。

我认为把所有的函数定义都放在头文件中是非常荒谬的。为什么?因为头文件被用作类的PUBLIC接口。这是“黑匣子”的外部。

当您需要查看一个类以引用如何使用它时,您应该查看头文件。头文件应该给出它能做什么的列表(注释以描述如何使用每个函数的细节),它应该包括一个成员变量列表。它不应该包括每个单独的函数是如何实现的,因为那是一船不必要的信息负载,只会使头文件混乱。