无论出于何种原因,我们公司都有一个编码指南,其中规定:

Each class shall have it's own header and implementation file.

所以如果我们写一个类叫做 MyString 我们需要一个关联的 MyStringh.hMyString.cxx.

还有其他人这样做吗?有没有人看到任何编译性能的影响?10000 个文件中的 5000 个类的编译速度是否与 2500 个文件中的 5000 个类的编译速度一样快?如果不是,差异明显吗?

[我们编写 C++ 代码并使用 GCC 3.4.4 作为我们的日常编译器]

有帮助吗?

解决方案

这里的术语是 翻译单位 并且您确实希望(如果可能的话)每个翻译单元有一个类,即每个 .cpp 文件有一个类实现,并具有相应的同名 .h 文件。

从编译/链接的角度来看,以这种方式执行操作通常更有效,特别是当您执行增量链接等操作时。这个想法是,翻译单元是隔离的,这样,当一个翻译单元发生变化时,您不必重建很多东西,而如果您开始将许多抽象概念集中到一个翻译单元中,则必须重建很多东西。

此外,您还会发现许多错误/诊断是通过文件名报告的(“Myclass.cpp 中的错误,第 22 行”),如果文件和类之间存在一对一的对应关系,则会有所帮助。(或者我想你可以称之为 2 对 1 对应)。

其他提示

被数千行代码淹没?

在一个目录中为每个类提供一组头文件/源文件似乎有点过分了。而如果班级数量达到100、1000,那就更让人害怕了。

但遵循“让我们把所有东西放在一起”的理念来研究资料来源后,得出的结论是,只有编写该文件的人才有希望不迷失其中。即使使用 IDE,也很容易错过一些东西,因为 当你使用 20,000 行的源代码时,你就会对任何与你的问题不完全相关的事情保持沉默。

现实生活中的例子:在这千行源代码中定义的类层次结构将自身封闭为钻石继承,并且子类中的某些方法被具有完全相同代码的方法覆盖。这很容易被忽视(谁愿意去探索/检查20,000行源代码?),并且当改变原始方法(错误修正)时,效果并不像例外的那么普遍。

依赖关系变得循环?

我在模板化代码中遇到了这个问题,但我在常规 C++ 和 C 代码中也看到了类似的问题。

将源代码分解为每个结构/类 1 个标头,您可以:

  • 加快编译速度,因为您可以使用符号前向声明而不是包含整个对象
  • 类之间存在循环依赖关系 (§)(即A类有一个指向B的指针,B有一个指向A的指针)

在源代码控制的代码中,类依赖关系可能会导致类在文件中定期上下移动,只是为了使头进行编译。在比较不同版本中的同一文件时,您不想研究此类动作的演变。

拥有单独的标头使代码更加模块化,编译速度更快,并且更容易通过不同版本差异研究其演变

对于我的模板程序,我必须将标头分为两个文件:.HPP 文件包含模板类声明/定义,.INL 文件包含所述类方法的定义。

将所有这些代码放入一个且仅一个唯一的标头中意味着将类定义放在该文件的开头,将方法定义放在结尾。

然后,如果有人只需要一小部分代码,使用仅一个标头的解决方案,他们仍然需要为较慢的编译付出代价。

(§) 请注意,如果您知道哪个类拥有哪个类,则可以在类之间具有循环依赖关系。这是关于了解其他类的存在的类的讨论,而不是共享指针循环依赖反模式。

最后一句话:标题应该是自给自足的

不过,多标头和多源的解决方案必须尊重一件事。

当您包含一个标头时,无论是哪个标头,您的源代码都必须干净地编译。

每个标题应该是自给自足的。您应该开发代码,而不是通过 grep 10,000 多个源文件项目查找宝藏,以查找哪个标头定义了您需要包含的 1,000 行标头中的符号,因为 枚举。

这意味着每个标头定义或前向声明它使用的所有符号,或者包含所有所需的标头(并且仅包含所需的标头)。

关于循环依赖的问题

下划线-d 问:

您能解释一下使用单独的标头对循环依赖有何影响吗?我认为不会。即使两个类都在同一个标​​头中完全声明,我们也可以轻松创建循环依赖项,只需在我们在另一个类中声明它的句柄之前提前声明一个类即可。其他一切似乎都很重要,但单独的标头促进循环依赖的想法似乎还很遥远

underscore_d,11 月 13 日 23:20

假设您有 2 个类模板,A 和 B。

假设 A 类的定义(分别为:B) 有一个指向 B 的指针(分别是A)。我们还可以说一下 A 类的方法(resp.B) 实际上调用 B 中的方法(resp.A)。

在类的定义及其方法的实现中都存在循环依赖。

如果A和B是普通类,并且A和B的方法在.CPP文件中,那么就不会有问题:您将使用前向声明,为每个类定义提供一个标头,然后每个 CPP 将包含两个 HPP。

但由于您有模板,您实际上必须重现上面的模式,但仅包含标题。

这意味着:

  1. 定义头 A.def.hpp 和 B.def.hpp
  2. 实现标头 A.inl.hpp 和 B.inl.hpp
  3. 为了方便起见,“天真的”标头 A.hpp 和 B.hpp

每个标头将具有以下特征:

  1. 在 A.def.hpp(分别是B.def.hpp),你有一个 B 类的前向声明(resp.A),这将使您能够声明该类的指针/引用
  2. A.inl.hpp(分别为B.inl.hpp)将包括 A.def.hpp 和 B.def.hpp,这将启用 A 中的方法(分别为:B)使用B类(分别)A)。
  3. A.hpp(分别为B.hpp) 将直接包含 A.def.hpp 和 A.inl.hpp (resp.B.def.hpp 和 B.inl.hpp)
  4. 当然,所有标头都需要自给自足,并受到标头防护装置的保护

天真的用户将包括 A.hpp 和/或 B.hpp,从而忽略整个混乱。

拥有这种组织意味着库编写者可以解决 A 和 B 之间的循环依赖关系,同时将两个类保留在单独的文件中,一旦您理解了该方案,就可以轻松导航。

请注意,这是一种边缘情况(两个模板彼此认识)。我希望大多数代码 不是 需要那个技巧。

我们在工作中这样做,如果类和文件具有相同的名称,则更容易找到内容。至于性能,您确实不应该在单个项目中拥有 5000 个类。如果这样做,可能需要进行一些重构。

也就是说,在某些情况下,我们在一个文件中有多个类。那就是当它只是文件主类的私有帮助器类时。

除了简单地“更清晰”之外,将类分离到单独的文件中还可以使多个开发人员更容易避免互相打扰。当需要向版本控制工具提交更改时,合并将会减少。

分离+1。我刚刚进入一个项目,其中某些类位于具有不同名称的文件中,或者与另一个类混在一起,并且不可能快速有效地找到这些类。您可以在构建中投入更多资源 - 您无法弥补程序员损失的时间,因为他找不到合适的文件进行编辑。

我工作过的大多数地方都遵循这种做法。实际上,我已经为 BAE(澳大利亚)编写了编码标准,以及为什么不只是在没有真正理由的情况下将某些东西刻在石头上的原因。

关于您有关源文件的问题,与其说需要花费大量时间进行编译,不如说更重要的是能够首先找到相关代码片段的问题。并不是每个人都在使用 IDE。与在一堆文件上运行“grep MyClass *.(h|cpp)”然后过滤掉 #include MyClass.h 语句相比,知道您只查找 MyClass.h 和 MyClass.cpp 确实节省了时间......

请注意,有一些解决方法可以解决大量源文件对编译时间的影响。请参阅 John Lakos 的《大规模 C++ 软件设计》进行有趣的讨论。

您可能还想阅读 Steve McConnell 的《Code Complete》,其中有关编码指南的精彩章节。事实上,这本书是一本很棒的书,我会经常回来阅读

这样做是常见的做法,特别是能够在需要它的文件中包含 .h。当然,性能会受到影响,但尽量不要考虑这个问题,直到它出现:)。
最好从分开的文件开始,然后尝试合并常用的 .h 文件,以提高性能(如果确实需要)。这一切都取决于文件之间的依赖关系,这对于每个项目来说都是非常特定的。

正如其他人所说,最佳实践是从代码维护和可理解性的角度将每个类放置在自己的翻译单元中。然而,在大型系统上,这有时是不可取的 - 请参阅标题为“使这些源文件更大”的部分 本文 Bruce Dawson 对权衡的讨论。

我发现这些准则在涉及头文件时特别有用:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Header_Files

每个文件只有一个类是非常有帮助的,但是如果您通过包含所有单独 C++ 文件的批量构建文件进行构建,则可以加快编译速度,因为对于许多编译器来说启动时间相对较长。

令我惊讶的是,几乎每个人都赞成每个班级有一个文件。问题是,在“重构”时代,人们可能很难保持文件名和类名同步。每次更改类名时,您也必须更改文件名,这意味着您还必须在包含文件的所有位置进行更改。

我个人将相关的类分组到一个文件中,然后给这样的文件一个有意义的名称,即使类名发生变化,该名称也不必更改。文件越少,滚动文件树就越容易。我在 Windows 上使用 Visual Studio,在 Linux 上使用 Eclipse CDT,两者都有快捷键可以直接进入类声明,因此查找类声明既简单又快捷。

话虽如此,我认为一旦一个项目完成,或者它的结构已经“固化”,并且名称更改变得很少,那么每个文件有一个类可能是有意义的。我希望有一个工具可以提取类并将它们放置在不同的 .h 和 .cpp 文件中。但我认为这不是必要的。

选择还取决于一个人所从事的项目类型。在我看来,这个问题不值得一个非黑即白的答案,因为任何一个选择都有优点和缺点。

同样的规则也适用于此,但它指出了一些允许的例外情况,如下所示:

  • 继承树
  • 仅在一个范围内使用的类 非常 范围有限
  • 一些实用程序只是放置在通用的“utils.h”中

两个字:奥卡姆剃刀。每个文件保留一个类,并将相应的标头放在单独的文件中。如果您这样做,例如为每个文件保留一项功能,那么您必须创建有关功能的构成的各种规则。每个文件保留一个类可以获得更多好处。而且,即使是半像样的工具也可以处理大量文件。保持简单,我的朋友。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top