头文件中的C ++代码

时间:2009-02-24 19:43:24

标签: c++ coding-style code-separation

我使用C ++的个人风格总是将类声明放在包含文件中,并将.cpp文件中的定义放在Loki's answer to C++ Header Files, Code Separation中。不可否认,我喜欢这种风格的部分原因可能与编码Modula-2和Ada的所有年份有关,两者都有与规范文件和正文文件类似的方案。

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

我并不像以前那样柔软,所以我并不急于拼抢他的这个潮流,直到我看到更多的人和他在一起。那个成语真的有多常见?

只是为答案提供一些结构:它现在是 The Way ,非常常见,有点普遍,不常见,还是出错?

17 个答案:

答案 0 :(得分:191)

您的同事错了,常见的方法是将代码放入.cpp文件(或您喜欢的任何扩展名)和标题中的声明。

将代码放入标题中偶尔会有一些优点,这可以让编译器更灵活地进行内联。但与此同时,它可能会破坏您的编译时间,因为每次编译器都包含所有代码时都必须处理它们。

最后,当所有代码都是标题时,通常会产生循环对象关系(有时是需要的)。

最重要的是,你是对的,他错了。

编辑:我一直在考虑你的问题。有一个一个的情况,他说的是真的。模板。许多较新的“现代”库(例如boost)大量使用模板,并且通常只是“标题”。但是,这只应在处理模板时完成,因为它是处理模板时唯一的方法。

编辑:有些人希望稍微澄清一下,这里有一些关于编写“仅限标题”代码的缺点的想法:

如果你四处搜索,你会看到很多人试图找到一种方法来减少处理boost时的编译时间。例如:How to reduce compilation times with Boost Asio,它看到包含boost的单个1K文件的14s编译。 14s可能似乎没有“爆炸”,但它肯定比典型的更长,并且可以很快加起来。在处理大型项目时。仅头文件库确实以一种非常可测量的方式影响编译时间。我们只是容忍它,因为助推是如此有用。

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

最后一点,当使用boost作为仅标题代码的示例时,通常会遗漏一个巨大的细节。

Boost是库,而不是用户级代码。所以它不会经常改变。在用户代码中,如果将所有内容放在标题中,每次稍微更改都会导致您必须重新编译整个项目。这是一个巨大的浪费时间(对于不会从编译变为编译的库不是这种情况)。当你在header / source和更好的方法之间进行分割时,使用forward声明来减少包含,你可以在一天内添加时节省数小时的重新编译。

答案 1 :(得分:142)

C ++编码人员同意 The Way ,羔羊将与狮子躺下,巴勒斯坦人将拥抱以色列人,猫狗将被允许结婚。

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

答案 2 :(得分:24)

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

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

答案 3 :(得分:19)

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

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

答案 4 :(得分:12)

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

我发现将所有内容放入标题中的有用内容是:

  1. 无需写作&同步标题和来源。

  2. 结构简单,没有循环依赖性迫使编码人员建立“更好”的结构。

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

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

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

    2. 编译速度比以前快得多。如果你有一个长时间高频率的项目,它可能表明你的项目设计有缺陷。将任务分成不同的项目和模块可以避免这个问题。

    3. 最后,我只是想以你个人的观点支持你的同事。

答案 5 :(得分:12)

通常我会将简单的成员函数放入头文件中,以允许它们被内联。但要将整个代码放在那里,只是为了与模板保持一致?这很简单。

请记住:A foolish consistency is the hobgoblin of little minds

答案 6 :(得分:6)

正如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的情况下是必需的,实现标头也是如此;)

答案 7 :(得分:5)

要添加更多乐趣,您可以添加包含模板实现的.ipp文件(包含在.hpp中),而.hpp包含界面。

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

答案 8 :(得分:5)

通常,在编写新类时,我会将所有代码放在类中,因此我不必查看其他文件。在一切正常后,我将方法的主体分解为cpp文件,将原型留在hpp文件中。

答案 9 :(得分:4)

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

// class-declaration

// inline-method-declarations

我不喜欢将方法的代码混合到类中,因为我觉得快速查找是很痛苦的。

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

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

答案 10 :(得分:4)

如果这种新方式真的是 The Way ,我们可能会在项目中遇到不同的方向。

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

我们也会在适用时使用"opaque pointer"-pattern

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

答案 11 :(得分:2)

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

答案 12 :(得分:2)

我将所有实现都放在了类定义之外。我希望从类定义中得到doxygen注释。

答案 13 :(得分:2)

我认为将所有函数定义放入头文件中是绝对荒谬的。为什么?因为头文件用作类的PUBLIC接口。它是&#34;黑盒子的外部&#34;。

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

答案 14 :(得分:1)

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

目前我正在开发一个非常复杂的神经网络模拟器,我期望使用的可接受的样式是:

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

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

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

答案 15 :(得分:0)

模板代码只能在标题中。除了内联之外,所有定义都应该是.cpp。对此最好的论据是遵循相同规则的std库实现。你不会不同意std lib开发人员对此的看法。

答案 16 :(得分:0)

我认为你的同事是正确的,只要他没有进入在标题中编写可执行代码的过程。 我认为,正确的平衡是遵循GNAT Ada指示的路径,其中.ads文件为其用户及其子项提供了完整的包定义。

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