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


195

我个人使用C ++的风格总是将类声明放在include文件中,并将定义放在.cpp文件中,这与Loki对C ++头文件代码分离的回答中所规定的非常相似。诚然,我喜欢这种样式的部分原因可能与我过去对Modula-2和Ada进行编码所花费的时间有关,这两种方式在规范文件和主体文件方面都有相似的方案。

我有一个同事,比我更懂C ++,他坚持认为所有C ++声明都应尽可能在头文件中包括这些定义。他并不是说这是一种有效的替代样式,甚至不是一种更好的样式,而是这是每个人现在都在使用C ++的新的普遍接受的样式。

我不像以前那样虚弱,所以我真的不急于爬上他的这个潮流,直到我看到更多的人和他在一起。那么这个成语到底有多普遍?

只是为了给出一些答案的结构:现在是The Way,很常见,有点常见,不常见或被bug淘汰吗?


标头中的单行函数(getter和setter)很常见。时间比古怪的第二眼更长。也许是为了完全定义一个小类,而该小类仅在同一标头中供另一个使用?
马丁·贝克特

到目前为止,我一直将我所有的类定义放在标头中。只有pimpl类的定义是例外。我只在标题中声明那些。
Johannes Schaub-litb

3
也许他认为是这样,因为这就是Visual C ++坚持要编写代码的方式。单击按钮时,将在头文件中生成实现。我不知道微软为什么会鼓励这样做,尽管出于其他原因在下面进行了解释。
WKS 2012年

4
@WKS-微软希望每个人都用C#编写程序,而在C#中,没有“标头”与“正文”的区别,它只是一个文件。在C ++和C#领域都存在很长时间了,C#方法实际上要容易得多。
Mark Lakata

1
@MarkLakata-这确实是他指出的一件事。我没有听说过这样的说法出来了最近,但IIRC他争辩说,Java和C#的工作这种方式,和C#是全新的时间,这使得它一个趋势所有的语言将很快成为继
TED

Answers:


209

您的同事是错误的,通常的方法是一直将代码放在.cpp文件(或您喜欢的任何扩展名)中,并将声明放在标头中。

有时将代码放入标头中有一些好处,这可以使编译器进行更巧妙的内联。但是同时,由于每次编译器都必须处理所有代码时,它可能会破坏编译时间。

最后,当所有代码都是头文件时,具有循环对象关系(有时是理想的)通常很烦人。

底线,你是对的,他是错的。

编辑:我一直在想你的问题。在一种情况下,他说的是真的。模板。许多新的“现代”库(例如boost)都大量使用模板,并且常常是“仅标头”。但是,仅应在处理模板时执行此操作,因为这是处理模板时的唯一方法。

编辑:有些人想要更多的说明,这是编写“仅标头”代码的缺点:

如果四处搜寻,您会发现很多人都在尝试寻找一种方法来减少处理boost的编译时间。例如:如何使用Boost Asio减少编译时间,这将看到包含boost的单个1K文件的14s编译。14s似乎没有“爆炸性”,但肯定比典型的要长得多,并且累积起来很快。处理大型项目时。仅标头的库确实以相当可衡量的方式影响编译时间。我们只是容忍它,因为升压非常有用。

此外,还有很多事情不能仅在标头中完成(即使boost具有某些线程,文件系统等某些部分都需要链接的库)。一个主要的例子是,您不能在仅标头的lib中拥有简单的全局对象(除非您诉诸于单例的可憎),否则会遇到多个定义错误。注意: C ++ 17的内联变量将使该特定示例在将来可行。

最后一点,当使用boost作为仅标头代码的示例时,经常会遗漏大量细节。

Boost是库,而不是用户级代码。因此它不会经常改变。在用户代码中,如果将所有内容放在标头中,则每个小的更改都将导致您不得不重新编译整个项目。这是巨大的时间浪费(对于不从编译到编译不变的库而言,情况并非如此)。当您在标头/源和更好之间进行拆分时,使用前向声明减少包含,可以在一整天的总和中节省重新编译的时间。


16
我很确定那是他从中得到的。每当出现这种情况时,他都会调出模板。他的论点大概是您应该以这种方式进行所有代码的一致性。
TED

14
这是一个很糟糕的论点,请坚持:)
Evan Teran

11
如果支持“ export”关键字,则模板定义可以在CPP文件中。据我所知,这是C ++的一个黑暗角落,通常大多数编译器甚至都没有实现。
安德烈·塔兰琴科

2
看到这个答案的底部(顶部有点费解)为例:stackoverflow.com/questions/555330/...
埃文特兰

3
对于“ Hooray,没有链接器错误”的讨论,它开始变得有意义。
埃文·特兰

156

C ++编码人员在The Way上达成共识的那天,羔羊将与狮子同卧,巴勒斯坦人将拥抱以色列人,猫和狗将被允许结婚。

此时,.h和.cpp文件之间的分隔几乎是任意的,这是编译器优化的痕迹。在我看来,声明属于标题,而定义属于实现文件。但是,这只是习惯,而不是宗教信仰。


139
“ C ++编码人员在The Way上达成共识的日子……”只剩下一个C ++编码人员!
Brian Ensink 09年

9
我以为他们已经就方式,.h中的声明和.cpp中的定义达成了共识
Hasen

6
我们都是盲人,C ++是一头大象。
Roderick Taylor

习惯?那么使用.h定义范围呢?被哪个东西取代了?
埃尔南Eche

28

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

在头文件中包含代码的原因是,通常需要关键字inline正常工作以及使用在其他cpp文件中实例化的模板时。


1
我认为这是最真实的原因;“它会在更改实际代码而不是声明时强制重新编译所有包含标头的文件”。还伴随着这样一个事实,即标头中的声明更改频率比.c文件中的实现更改频率低。
Ninad

20

可能会通知您的同事的想法是,大多数C ++代码应模板化以实现最大的可用性。而且,如果已将其模板化,则所有内容都必须位于头文件中,以便客户端代码可以看到并实例化它。如果对Boost和STL足够好,那么对我们也足够。

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


我认为您对此是正确的。当我们讨论它时,他总是使用模板示例,您或多或少必须这样做。我也不同意“必须”,但是我的选择相当复杂。
TED

1
@ted-对于模板代码,您确实需要将实现放在标头中。关键字'export'允许编译器支持模板的声明和定义的分隔,但是对导出的支持几乎不存在。anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr,2009年

一个头,是的,但它并不必须是相同的标题。请参阅下面的未知答案。
TED

这是有道理的,但是我不能说我曾经遇到过这种风格。
Michael Burr,2009年

14

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

我发现将所有内容放入标头的有用之处在于:

  1. 无需编写和同步标题和源。

  2. 结构是简单的,没有循环依赖关系会迫使编码器形成“更好”的结构。

  3. 可移植,易于嵌入到新项目中。

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

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

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

最后,就我个人而言,我只想支持您的同事。


3
+1。除了一个人,您没有一个想法,就是在标头中,仅项目较长的编译时间可能暗示过多的依赖关系,这是不好的设计。好点子!但是,可以将这些依赖项删除到实际上编译时间很短的程度吗?
TobiMcNamobi '16

@TobiMcNamobi:我喜欢“懈怠”的想法,以获得对不良设计决策的更好反馈。但是,在仅标头与单独编译的情况下,如果我们坚持这一想法,那么最终将只有一个编译单元和大量编译时间。即使设计确实很棒。
乔·苏

换句话说,接口和实现之间的分离实际上是设计的一部分。在C语言中,您需要通过头和实现中的分离来表达有关封装的决策。
乔·苏

1
我开始怀疑是否像现代语言一样完全丢弃标头是否有任何缺点。
乔苏因此

12

通常,我会将一些琐碎的成员函数放入头文件中,以使它们内联。但是要将整个代码放在那里,只是为了与模板保持一致?简直是疯了。

记住:愚蠢的一致性是小头脑的妖精


是的,我也是。我使用的一般规则似乎类似于“如果适合一行代码,则将其保留在标题中”。
TED

当库A<B>在cpp文件中提供模板类的主体,然后用户想要一个时,会发生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

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

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


这也是我对模板定义所做的事情(尽管我不确定我是否使用相同的扩展名……已经有一段时间了)。
TED

5

通常,在编写新类时,我会将所有代码放入该类中,因此不必在另一个文件中查找它。.一切正常之后,我将方法的主体分解为cpp文件。 ,将原型保留在hpp文件中。


4

如果这种新方法确实是The Way,那么我们的项目可能会朝着不同的方向发展。

因为我们试图避免标题中所有不必要的东西。这包括避免头级联。标头中的代码可能需要包含一些其他标头,这将需要另一个标头,依此类推。如果我们被迫使用模板,我们将尽量避免在模板头上乱扔标题。

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

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


4

我个人在头文件中执行此操作:

// class-declaration

// inline-method-declarations

我不喜欢将方法的代码与类混在一起,因为我发现快速查找内容很麻烦。

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

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


2

恕我直言,他只有在做模板和/或元编程时才有优点。已经有很多原因提到您将头文件限制为仅声明。它们就是...标题。如果要包含代码,则将其编译为库并链接起来。


2

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


1
我知道已经晚了,但是反对者(或支持者)很想评论为什么?对我来说,这似乎是一个合理的声明。我们使用Doxygen,肯定会出现问题。
TED

2

我认为将所有函数定义放入头文件中绝对是荒谬的。为什么?因为头文件用作类的PUBLIC接口。这是“黑匣子”的外部。

当需要查看一个类以引用如何使用它时,应查看头文件。头文件应提供其功能的列表(注释以描述如何使用每个函数的详细信息),并且应包括成员变量的列表。它不应该包括如何实现每个单独的功能,因为那是一堆不必要的信息,只会使头文件杂乱无章。


1

这不是真的取决于系统的复杂性以及内部惯例吗?

目前,我正在开发一个极其复杂的神经网络模拟器,并且我期望使用的公认样式是:

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

这将用户构建的仿真与开发人员构建的基类分离开来,并且在这种情况下效果最佳。

但是,我很惊讶地看到人们在图形应用程序或任何其他旨在不为用户提供代码库的应用程序中执行此操作。


1
“类代码”和“可执行代码”之间到底有什么区别?
TED

就像我说的那样,它是一个神经模拟器:用户创建可执行模拟,该模拟建立在充当神经元等的大量类之上。因此,我们的代码就是简单的类,它们本身无法真正执行任何操作,并且用户创建了可执行代码这使得模拟器可以完成任务。
Ed James

通常,您不能对大多数程序的绝大多数(如果不是全部)说“实际上不能独自做任何事情”吗?您是在说“主要”代码放在cpp中,但没有其他作用吗?
TED

在这种情况下,情况有所不同。我们编写的代码基本上是一个库,用户可以在此库的基础上构建它们的仿真,这些仿真实际上是可运行的。像openGL一样考虑->您可以获得许多函数和对象,但是没有可以运行它们的cpp文件,它们是无用的。
Ed James

0

模板代码应仅在标题中。除此之外,除内联外,所有定义均应使用.cpp。最好的说法是遵循相同规则的std库实现。您不会不同意std lib开发人员对此的看法。


哪个标准库?GCC libstdc++似乎(AFAICS)几乎什么都没放src,几乎所有内容都放进去了include,无论它是否“必须”放在标题中。因此,我认为这不是准确/有用的引用。无论如何,我认为stdlibs并不是用户代码的典范:它们显然是由熟练的编码人员编写的,但要使用而不是阅读:它们抽象出了大多数编码人员无需考虑的高度复杂性,_Reserved __names到处都需要丑陋,以免与用户发生冲突,注释和空格低于我建议的水平,等等。它们仅是狭窄的示例。
underscore_d

0

我认为您的同事是对的,只要他不参与在标头中编写可执行代码的过程即可。我认为,正确的平衡点是遵循GNAT Ada指示的路径,在该路径中.ads文件为用户及其子级用户提供了一个非常完善的包接口定义。

顺便说一下,Ted,您是否在这个论坛上浏览了有关Ada绑定到您几年前编写的CLIPS库的最新问题,现在该问题不再可用(相关网页现已关闭)。即使是旧的Clips版本,此绑定对于愿意在Ada 2012程序中使用CLIPS推理引擎的人来说也是一个很好的入门示例。


1
大声笑。2年后,这是结识某人的怪异方法。我会检查我是否还有副本,但很可能没有。我这样做是为了参加AI类课程,因此我可以在Ada中编写代码,但是故意将项目CC0(基本上没有版权)制作为希望有人能无耻地接受它并对其进行处理。
TED
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.