头文件中的多个类与每个类的单个头文件

无论出于何种原因,我们公司都有一个编码准​​则,规定:

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文件。

从编译/链接的angular度来看,这样做通常更有效率,特别是如果您正在执行增量链接等操作时。 这个想法是,翻译单位是孤立的,当一个翻译单位改变时,你不必重build很多的东西,因为如果你开始把许多抽象概括到一个单一的翻译单位,你将不得不重build。

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

数千行代码被淹没了

在一个目录中每个类有一套头文件/源文件可能看起来过分。 如果class级人数达到100人或1000人,甚至会令人恐惧。

但是根据“让我们把所有东西放在一起”这个哲学来玩弄资料来源,结论就是只有那个写这个文件的人才有希望不要迷失在里面。 即使使用IDE,也很容易错过任何东西,因为当你使用20000行的源代码时,只需关注你的想法,而不是完全引用你的问题。

现实生活中的例子:在这千行代码中定义的类层次结构本身就是一个钻石inheritance,而且有些方法在代码完全相同的子类中被覆盖。 这很容易被忽略(谁想要探索/检查一个20000行的源代码?),当原来的方法被改变(错误修正)时,效果不如通用。

依赖性变成循环?

我有模板代码的这个问题,但我看到类似的问题与常规的C + +和C代码。

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

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

在源代码控制的代码中,类的依赖关系可能导致文件上下移动类,只是为了编译头文件。 比较不同版本的相同文件时,您不想研究这种移动的演变。

拥有单独的头文件使得代码更加模块化,编译更快,并且通过不同的版本差异来更容易地研究它的演变

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

把所有这些代码放在一个唯一的头文件中,意味着在这个文件的开始处放置类定义,在末尾放置方法定义。

然后,如果有人只需要一小部分的代码,只用一个头文件的解决scheme,他们仍然需要支付较慢的编译。

(§)注意,如果你知道哪个类拥有哪个类,你可以在类之间有循环依赖关系。 这是关于其他类存在知识的类的讨论,而不是shared_ptr循环依赖关系反模式。

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

但是,有一件事必须由多个头文件和多个源文件的解决scheme来解决。

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

每个标题应该是自给自足的。 你应该开发代码,而不是通过grep你的10,000多个源文件项目find哪个头定义在1000行头你需要包括只是因为一个枚举的符号寻宝。

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

关于循环依赖的问题

下划线问:

你能解释一下如何使用单独的头文件对循环依赖有什么不同吗? 我不这么认为。 即使两个类都在同一个头部完全声明,我们也可以简单地创build一个循环依赖关系,只要在我们声明另一个类的句柄之前提前声明一个。 其他的一切似乎都很棒,但是单独的头文件有助于实现循环依赖的想法似乎没有任何意义

underscore_d,11月13日23:20

假设你有两个类模板,A和B.

假设类A(或B)的定义有一个指向B(或A)的指针。 还有,A类(或B类)的方法实际上是从B(或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(分别为B.def.hpp和B.inl.hpp)
  4. 当然,所有的头文件都需要自给自足,并由头文件保护

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

拥有该组织意味着图书馆编写者可以解决A和B之间的循环依赖关系,同时将两个类保存在不同的文件中,一旦理解了scheme,便于浏览。

请注意,这是一个边缘案例(两个模板相互认识)。 我希望大多数代码不需要这个技巧。

我们在工作中这样做,如果类和文件具有相同的名称,则更容易find它们。 至于performance,你真的不应该在一个单一的项目5000个class。 如果你这样做,一些重构可能是为了。

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

除了“更清晰”之外,将课程分成单独的文件使得多个开发者更容易不要踩在彼此的脚趾上。 当需要将更改提交到版本控制工具时,合并将会减less。

+1分离。 我刚刚进入一个项目,其中一些class级的文件名称不同,或者与其他class级混在一起,不可能以快速有效的方式find这些class级。 你可以在构build中投入更多的资源 – 你不能弥补丢失的程序员时间,因为他找不到合适的文件进行编辑。

天儿真好,

我工作的大部分地方都遵循这种做法。 实际上,我已经为BAE(澳大利亚)编写了编码标准,以及为什么没有真正的理由去刻石头的原因。

关于源文件的问题,编译没有多less时间,但是更重要的是能够首先find相关的代码片段。 并不是每个人都在使用IDE。 而且知道你只是寻找MyClass.h和MyClass.cpp,相比于在一堆文件上运行“grep MyClass *。(h | cpp)”,然后过滤掉#include MyClass.h语句,真的可以节省时间…

请注意,大量源文件对编译时间的影响是有效的。 参见John Lakos的大规模C ++软件devise进行有趣的讨论。

您也可以阅读Steve McConnell撰写的Code Complete,了解关于编码准则的精彩篇章。 实际上,这本书是我经常回来的一个很好的阅读

欢呼声,罗布

正如其他人所说,最好的做法是从代码维护和可理解性的angular度将每个类放在自己的翻译单元中。 然而,在大规模的系统中,这有时是不可取的 – 请参阅Bruce Dawson在本文中题为“使这些源文件变大”的部分,以讨论权衡。

这是常见的做法,特别是要能够在需要的文件中包含.h。 当然,性能会受到影响,但在出现问题之前不要去思考这个问题:)。
最好先分开文件,然后尝试合并常用的.h,以便在需要时提高性能。 这一切都归结为文件之间的依赖关系,这对每个项目都是非常具体的。

对于头文件,我发现这些指南特别有用: http : //google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Header_Files

每个文件只有一个类是非常有帮助的,但是如果通过包含所有单独C ++文件的bulkbuild文件进行构build,则对于许多编译器而言,由于启动时间相对较长,因此编译速度更快。

我很惊讶,几乎每个人都赞成每个class级有一个文件。 问题在于,在“重构”时代,可能很难将文件和类名保持同步。 每次更改类名称时,都必须更改文件名,这意味着您还必须在包含文件的任何地方进行更改。

我个人将相关的类组合成单个文件,然后给这样的文件一个有意义的名字,即使类名更改也不会改变。 文件较less也使滚动文件树更容易。 我在Windows上使用Visual Studio,在Linux上使用Eclipse CDT,两者都有快捷键,可以直接进入类声明,因此find类声明非常简单快捷。

话虽如此,我认为一旦一个项目完成,或者其结构“固化”,名称变化变得罕见,那么每个文件就有一个类是有意义的。 我希望有一个工具,可以提取类,并将其放置在不同的.h和.cpp文件中。 但我不认为这是必不可less的。

select也取决于一个项目的types。 在我看来,这个问题不值得一个黑色和白色的答案,因为任何select有利有弊。

这里也适用相同的规则,但是在允许的情况下它会注意到一些例外情况如此:

  • inheritance树
  • 仅在非常有限的范围内使用的类
  • 一些公用事业只是放在一个普遍的'utils.h'