在头文件中放置C++定义是一个好的实践吗?

261

我的个人C ++风格一直是将类声明放在包含文件中,将定义放在.cpp文件中,非常像Loki对于 C ++ Header Files,Code Separation的回答中建议的那样。不可否认,我喜欢这种风格的原因可能与我花费的所有年份编写Modula-2和Ada有关,两者都具有规范和主体文件的类似方案。

我有一个比我更了解C ++的同事,他坚称所有C ++声明都应该在可能的情况下在头文件中直接包含定义。他并不是说这是一种有效的备用风格,甚至不是稍微好一点的风格,而是这是每个人现在都在为C ++使用的新普遍接受风格。

我不像以前那么灵活,所以我真的不急着爬上他的“车轮战车” ,直到我看到还有几个人和他一起上去。那么这个习惯用语到底有多常见?

只是为了给答案一些结构:它现在是The Way™,非常普遍,有些普遍,不太普遍还是很疯狂?


2
在头文件中编写一行函数(包括getter和setter)很常见,但如果一个函数太长了,则可能会引起注意。这种情况或许适用于小类的完整定义,而该类仅被同一头文件中的另一个类使用。 - Martin Beckett
到目前为止,我一直将所有的类定义放在头文件中。只有 Pimpl 类的定义是例外,我只在头文件中声明它们。 - Johannes Schaub - litb
3
他可能认为这是正确的方式,因为Visual C++坚持要求代码以这种方式编写。当你点击一个按钮时,实现会在头文件中生成。虽然其他人已经解释了原因,但我不知道微软为什么会鼓励这种做法。 - W.K.S
7
微软更希望每个人都使用 C# 进行编程,在 C# 中没有“头文件”和“源文件”的区别,只有一个文件。我在 C++ 和 C# 领域都有很长时间的经验,实际上,C# 的方式要容易处理得多。 - Mark Lakata
1
@MarkLakata - 这确实是他指出的其中一件事。我最近没有听到他提出这个论点,但我记得他认为Java和C#是这样工作的,而当时C#是全新的,这使其成为所有语言即将遵循的趋势。 - T.E.D.
你可以使用显式模板实例化并导出显式模板实例化,将一些模板代码移动到cpp文件中。例如,如果你在不同的单元中多次使用vector<int>,你可以有一个cpp文件来显式实例化它,然后避免在其他翻译单元中重新实例化的开销。 - Mani Zandifar
17个回答

262

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

有时把代码放在头文件中的确有一些优点,这可以让编译器更聪明地内联。但与此同时,它也可能破坏你的编译时间,因为每次编译器包含代码时都必须处理所有代码。

最后,当所有代码都在头文件中时,循环对象关系(有时是期望的)经常会成为问题。

总之,你是正确的,他是错误的。

编辑:我考虑了你的问题。有一个情况是他说的是对的。模板。许多新的“现代”库如 boost 等大量使用模板,并且通常是“仅头文件”。然而,只有处理模板时才应该这样做,因为这是处理它们的唯一方法。

编辑:有些人想要更多的澄清,下面是有关编写“仅头文件”的缺点的一些想法:

如果你搜索一下,你会发现很多人试图找到一种减少处理 boost 时编译时间的方法。例如:如何通过 Boost Asio 减少编译时间,其中一个包含 boost 的单个 1K 文件的编译需要 14 秒。14 秒可能并不会爆炸,但它肯定比典型值要长得多,并且在处理大型项目时可以快速累积。仅头文件库确实以相当可测量的方式影响编译时间。我们只是容忍了它,因为 boost 很有用。

此外,有许多事情在头文件中无法完成(即使boost也需要链接到某些库,如线程、文件系统等的某些部分)。一个主要的例子是,在仅限头文件的库中无法使用简单的全局对象(除非您采用单例模式这样的可怕方法),否则会遇到多次定义错误。 注意:C++17中的内联变量将使未来可以实现此特定示例。

最后一点,当将boost作为仅限头文件代码的示例时,通常会忽略一个重要细节。

Boost是一个库,而不是用户级别的代码。因此,它不经常更改。在用户代码中,如果你把所有东西都放在头文件中,每个小改动都会导致你不得不重新编译整个项目。这是一种巨大的时间浪费(对于不会从一个编译到另一个编译发生变化的库而言不是这种情况)。当您将事物分割在头文件/源文件之间,并更好地使用前向声明以减少包含,您可以节省数小时的重新编译时间,这在一天内增加起来非常可观。


18
我很确定那是他的观点来源。每次提到这件事,他都会提到模板。他的论点大致是为了保持一致性,你应该以这种方式编写所有代码。 - T.E.D.
15
他提出的是一个蹩脚的论点,坚持自己的立场 :) - Evan Teran
12
如果支持"export"关键字,模板定义可以在CPP文件中。据我所知,“export”是C++的一个黑暗角落,大多数编译器甚至没有实现它。 - Andrei Taranchenko
3
"从“太棒了,没有链接错误”开始,这段内容对于这个讨论变得有意义。" - Evan Teran
2
@djechlin,最后一个要点,你错过了一个重要的细节。Boost是,而不是用户级别的代码。因此它不经常更改。在用户代码中,如果您将所有内容都放在头文件中,每个小更改都会导致您必须重新编译整个项目。这是一种巨大的时间浪费(对于从编译到编译不会更改的库来说并非如此)。当您在头文件/源文件之间分割事物,并使用前向声明来减少包含时,您可以节省数小时的重新编译时间,这些时间加起来可能是一天。 - Evan Teran
显示剩余14条评论

199

当C++程序员们达成共识时,HTML标记内的说法,羔羊将与狮子躺下,巴勒斯坦人将拥抱以色列人,猫和狗将被允许结婚。

.h和.cpp文件之间的分离在这个点上大多是任意的,是编译器优化的残余。在我看来,声明应该在头文件中,定义应该在实现文件中。但是,这只是习惯,不是教条。


177
如果C++程序员都能就“正确的方式”达成一致,那么只会剩下一个C++程序员! - Brian Ensink
12
我认为他们已经就.h文件中声明,.cpp文件中定义的方式达成了一致。 - hasen
10
我们都是盲人,而C++就像一只大象。 - Roderick Taylor
习惯?那么使用.h来定义作用域怎么样?它被什么替代了? - Hernán Eche

37

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

头文件中编写代码的原因之一是它通常需要使用关键字inline才能正常工作,并且在使用模板被实例化在其他cpp文件中时也需要。


7
当您更改实际代码而不是声明时,它会强制重新编译包含头文件的所有文件。我认为这是最真实的原因;也与头文件中的声明比.c文件中的实现更少更改相符。 - Ninad

27
你的同事可能得到的信息是,大多数C++代码都应该使用模板来实现最大的可用性。如果代码被模板化,则所有内容都需要在头文件中,以便客户端代码可以看到并进行实例化。如果这对Boost和STL足够好,那么这对我们也足够好。
我不同意这种观点,但这可能是他的立场。

我认为你说得对。每次我们讨论这个问题时,他总是拿模板的例子来说明,而在那种情况下你或多或少是必须这样做的。我也不同意“必须”这个说法,但我的替代方案比较复杂。 - T.E.D.
1
@ted - 对于模板化的代码,您确实需要将实现放在头文件中。 'export' 关键字允许编译器支持模板的声明和定义分离,但对 export 的支持几乎不存在。http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf - Michael Burr
一个头文件,是的,但它不一定是相同的头文件。请参见下面 unknown 的答案。 - T.E.D.
这很有道理,但我以前没有遇到过这种风格。 - Michael Burr

15

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

我发现把所有东西都放在头文件中有以下几个好处:

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

  2. 结构简单,没有循环依赖迫使程序员制定“更好”的结构。

  3. 易于移植到新项目中。

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

  1. 更改源文件很可能会更改头文件,这将导致整个项目被重新编译。

  2. 编译速度比以前快得多。如果您有一个需要长时间和高频率构建的项目,则可能表明您的项目设计存在缺陷。将任务分解成不同的项目和模块可以避免此问题。

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


3
只有你才有这样的想法:在仅有头文件的项目中,长时间的编译时间可能暗示着依赖关系过多,这是不好的设计。这个观点挺好的!但是这些依赖关系能否被移除到一个使得编译时间实际上变短的程度? - TobiMcNamobi
@TobiMcNamobi:我喜欢“懒惰”这个想法,以便获得有关糟糕设计决策的更好反馈。然而,在仅包含头文件与分别编译之间的情况下,如果我们采用这个想法,我们最终会得到一个单一的编译单元和巨大的编译时间。即使设计实际上是很好的。 - Jo So
换句话说,接口和实现之间的分离实际上是你的设计的一部分。在C语言中,你需要通过头文件和实现文件之间的分离来表达关于封装的决策。 - Jo So
1
我开始怀疑是否完全像现代语言一样放弃头文件是否有任何缺点。 - Jo So

12
通常我会把微不足道的成员函数放在头文件中,以使它们可以被内联。但是仅仅为了与模板保持一致而将整个代码主体放在那里?那就太疯狂了。
请记住:愚蠢的一致性是小人物的鬼怪

是的,我也这样做。我通常使用的一般规则似乎是“如果代码可以放在一行中,则将其留在头文件中”。 - T.E.D.
当一个库在cpp文件中提供了模板类A<B>的主体,然后用户想要一个A<C>时会发生什么? - jww
@jww 我没有明确说明,但是模板类应该在头文件中完全定义,以便编译器可以使用任何类型来实例化它。这是一个技术要求,而不是风格选择。我认为原始问题的问题在于有人决定如果对模板有好处,那么对普通类也有好处。 - Mark Ransom

7
正如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

这里的救星是大多数情况下前向头文件都是无用的:只有在typedeftemplate的情况下才是必需的,实现头文件也是如此;)

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

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

// class-declaration

// inline-method-declarations

我不喜欢将方法的代码与类混合在一起,因为我觉得这样很难快速查找。

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

在类中编写方法是有效的...但从可读性的角度来看,我不喜欢这样做。将方法放在头文件中意味着,在可能的情况下,它们会被内联。


6
为了增加乐趣,您可以添加包含模板实现的.ipp文件(这些文件被包含在.hpp中),而.hpp则包含接口。
除了依赖于项目的模板化代码(可能占大多数或少数文件)之外,还有一些正常的代码,因此最好将声明和定义分开。在需要的地方提供前向声明 - 这可能会影响编译时间。

这也是我对模板定义所做的(尽管我不确定我是否使用了相同的扩展名...已经有一段时间了)。 - T.E.D.

6

通常在编写新类时,我会将所有代码放在类中,这样我就不必在另一个文件中查找它了。等一切都正常工作后,我将方法的主体拆分到cpp文件中,只留下hpp文件中的原型。


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