是否值得向前宣布图书馆类?

时间:2009-04-26 16:15:52

标签: c++ forward-declaration

我刚开始学习Qt,使用他们的教程。我目前正在使用教程7,我们已经制作了一个新的LCDRange类。 LCDRange(.cpp文件)的实现使用Qt QSlider类,因此在.cpp文件中

#include <QSlider>

但是在标题中是一个前向声明:

class QSlider;

根据Qt,

  

这是另一个经典技巧,但经常使用的技巧要少得多。因为我们在类的接口中不需要QSlider,所以只在实现中,我们在头文件中使用类的前向声明,并在.cpp文件中包含QSlider的头文件。

     

这使得大项目的编译速度更快,因为编译器通常花费大部分时间来解析头文件,而不是实际的源代码。仅这一技巧通常可以将编辑速度提高两倍或更多。

这值得吗?这似乎是有道理的,但是要跟踪它还有一件事 - 我觉得将所有内容都包含在头文件中要简单得多。

8 个答案:

答案 0 :(得分:17)

绝对。 C / C ++构建模型是... ahem ...一个时代错误(说最好的)。对于大型项目,它将成为一个严肃的PITA。

正如Neil所说,这应该是您课堂设计的默认方法,除非您真的需要,否则不要偏离。

打破循环包括引用是您必须使用前向声明的一个原因。

// a.h
#include "b.h"
struct A { B * a;  }

// b.h
#include "a.h"  // circlular include reference 
struct B { A * a;  }

// Solution: break circular reference by forward delcaration of B or A

缩短重建时间 - 想象一下以下代码

// foo.h
#include <qslider>
class Foo
{
   QSlider * someSlider;
}

现在,直接或间接引入Foo.h的每个.cpp文件也会引入QSlider.h及其所有依赖项。这可能是数百个.cpp文件! (预编译的头文件有点帮助 - 有时很多 - 但它们会在内存/磁盘压力下转动磁盘/ CPU压力,因此很快达到“下一个”限制)

如果标题仅需要引用声明,则此依赖关系通常可以限于几个文件,例如Foo.cpp中。

减少增量构建时间 - 在处理您自己的(而不是稳定的库)标头时,效果更加明显。想象一下你有

// bar.h
#include "foo.h"
class Bar 
{
   Foo * kungFoo;
   // ...
}

现在如果你的大部分.cpp需要拉入bar.h,他们也会间接地引入foo.h.因此,foo.h的每次更改都会触发所有这些.cpp文件的构建(甚至可能不需要知道Foo!)。如果bar.h使用Foo的前向声明,则对foo.h的依赖性仅限于bar.cpp:

// bar.h
class Foo;
class Bar 
{
   Foo * kungFoo;
   // ...
}

// bar.cpp
#include "bar.h"
#include "foo.h"
// ...

它是一种常见的模式 - PIMPL pattern。它的用途是双重的:首先它提供真正的接口/实现隔离,另一个是减少构建依赖性。在实践中,我会将它们的用处加权50:50。

标题中需要引用,不能直接实例化依赖类型。这限制了可以应用前向声明的情况。如果你明确地这样做,那么通常使用实用程序类(例如boost::scoped_ptr)。

构建时间值得吗? Definitely,我会说。在最坏的情况下,构建时间会随着项目中的文件数量而增加多项式。其他技术 - 比如更快的机器和并行构建 - 只能提供百分比增益。

构建越快,开发人员测试他们所做的事情越多,单元测试运行的次数越多,可以找到更快的构建中断,并且开发人员最不会拖延。

在实践中,管理您的构建时间虽然对于大型项目(例如,数百个源文件)至关重要,但它仍然会对小型项目产生“舒适的差异”。此外,在事实之后添加改进通常是耐心的练习,因为单个修复可能仅在40分钟构建的几秒(或更少)中削减。

答案 1 :(得分:8)

我一直都在使用它。我的规则是,如果它不需要标题,那么我提出了一个前向声明(“如果你必须使用标题,如果你可以使用前向声明”)。唯一糟糕的是我需要知道如何声明类(struct / class,如果它是模板,我需要它的参数,......)。但在绝大多数情况下,它只是归结为"class Slider;"或其他东西。如果某些事情需要更多麻烦才能被宣布,那么总是可以声明一个特殊的前向声明标题,就像标准对iosfwd那样。

不包括头文件不仅会减少编译时间,还会避免污染命名空间。包括标题在内的文件会感谢您尽可能少地包含,以便他们可以继续使用干净的环境。

这是一个粗略的计划:

/* --- --- --- Y.hpp */
class X;
class Y {
    X *x;
};

/* --- --- --- Y.cpp */
#include <x.hpp>
#include <y.hpp>

...

有一些智能指针专门用于处理指向不完整类型的指针。一个众所周知的是boost::shared_ptr

答案 2 :(得分:3)

是的,确实有帮助。如果你担心编译时间,那么添加到你的保留节目的另一件事是预编译标题。

查找FAQ 39.1239.13

答案 3 :(得分:2)

标准库对标准头<iosfwd>中的一些iostream类执行此操作。但是,它不是一种普遍适用的技术 - 注意其他标准库类型没有这样的标题,它不应该(恕我直言)是你设计类层次结构的默认方法。

虽然这对于程序员来说是最受欢迎的“优化”,但我怀疑,与大多数优化一样,他们中的很少人实际上已经计划了他们的项目的构建,无论是否有这样的声明。我在这方面的有限实验表明,在现代编译器中使用预编译的头文件使其不必要。

答案 4 :(得分:1)

较大项目的编译时间存在巨大差异,即使是具有谨慎管理依赖项的项目也是如此。你最好养成向前声明并尽可能多地保留头文件的习惯,因为在很多使用C ++的软件商店都需要它。你在标准头文件中看不到这么多的原因是因为那些大量使用模板,此时声明变得很难。对于MSVC,您可以使用/ P来查看在实际编译之前预处理文件的外观。如果你没有在你的项目中做过任何前瞻声明,那么看看需要做多少额外的处理可能是一个有趣的经历。

答案 5 :(得分:0)

一般来说,没有。

我曾经尽可能多地向前宣布,但不再是。

就Qt而言,您可能会注意到有一个<QtGui>包含文件将引入所有GUI小部件。此外,还有<QtCore><QtWebKit><QtNetwork>等。每个模块都有一个头文件。似乎Qt团队认为这也是首选方法。他们在模块文档中这么说。

是的,编译时间可能会增加。但根据我的经验,它并没有那么多。如果是这样的话,那么使用预编译的头文件就是下一步。

答案 6 :(得分:0)

当你写...

包括“foo.h”

...你从而指示一个传统的构建系统“任何时候在库文件foo.h中有任何改变,丢弃这个编译单元并重建它,即使foo.h发生的所有事情都是评论,或对foo.h包含的某个文件添加评论;即使发生的一切都是一些超级挑剔的同事重新平衡花括号;即使没有发生任何事情,除了在foo.h中检查的压力同事没有改变,无意中改变了它的时间戳。“

为什么要发出这样的命令?库标题,因为它们通常比应用程序标题具有更多的人类读者,对于对二进制文件没有影响的更改具有特殊的漏洞,例如改进的函数和参数文档或版本号或版权日期的颠覆。

C ++规则允许命名空间在编译单元中的任何位置重新打开(与 struct 不同)支持前瞻性声明。

答案 7 :(得分:-1)

前向声明对于打破循环依赖非常有用,有时可以与您自己的代码一起使用,但将它们与库代码一起使用可能会破坏另一个平台上的程序或其他版本的库(这将发生)即使你的代码,如果你不够小心)。恕我直言不值得。