8

我刚刚开始学习 Qt,使用他们的教程。我目前正在学习教程 7,我们在其中创建了一个新的 LCDRange 类。LCDRange(.cpp文件)的实现使用Qt QSlider类,所以在.cpp文件中是

#include <QSlider>

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

class QSlider;

根据 Qt,

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

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

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

4

8 回答 8

17

绝对地。C/C++ 构建模型是……咳咳……不合时宜(最好说)。对于大型项目,它变成了一个严肃的 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模式。它的用途有两个:首先它提供真正的接口/实现隔离,另一个是减少构建依赖关系。在实践中,我会以 50:50 衡量它们的有用性。

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

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

构建速度越快,开发人员测试他们所做的事情的频率越高,单元测试运行的频率越高,构建中断的修复速度就越快,开发人员最终拖延的情况就越少。

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

于 2009-04-26T16:50:34.720 回答
8

我用它所有的时间。我的规则是,如果它不需要标题,那么我提出一个前向声明(“如果必须使用标题,如果可以使用前向声明”)。唯一糟糕的是我需要知道类是如何声明的(结构/类,也许如果它是一个模板我需要它的参数,......)。但在绝大多数情况下,它只是归结为类似的"class Slider;"东西。如果某些事情需要更多的麻烦才能被声明,人们总是可以像标准一样声明一个特殊的前向声明头iosfwd

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

这是粗略的计划:

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

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

...

有一些智能指针专门设计用于处理指向不完整类型的指针。一个非常有名的是boost::shared_ptr.

于 2009-04-26T16:51:27.697 回答
3

是的,它确实有帮助。如果您担心编译时间,要添加到您的曲目中的另一件事是预编译头文件。

查找常见问题解答 39.1239.13

于 2009-04-26T16:20:41.950 回答
2

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

尽管这似乎是程序员最喜欢的“优化”,但我怀疑像大多数优化一样,他们中很少有人真正为他们的项目的构建计时,无论是否有这样的声明。我在这方面的有限实验表明,在现代编译器中使用预编译头文件使其变得不必要。

于 2009-04-26T16:27:28.627 回答
1

大型项目的编译时间存在巨大差异,即使是具有精心管理的依赖关系的项目。您最好养成前向声明的习惯,并尽可能避免头文件,因为在许多使用 C++ 的软件商店中,它是必需的。您在标准头文件中看不到它的原因是因为它们大量使用模板,此时向前声明变得困难。对于 MSVC,您可以使用 /P 在实际编译之前查看预处理文件的外观。如果您没有在项目中进行任何前向声明,那么看看需要完成多少额外处理可能会是一次有趣的经历。

于 2009-04-26T17:03:12.457 回答
0

一般来说,没有。

我曾经尽可能多地转发声明,但不再是。

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

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

于 2009-04-26T16:55:31.710 回答
0

当你写...

包括“foo.h”

...您因此指示传统的构建系统“任何时候库文件 foo.h 中有任何更改,丢弃此编译单元并重新构建它,即使 foo.h 发生的所有事情都是添加注释,或在 foo.h 包含的某个文件中添加注释;即使发生的所有事情都是一些极其挑剔的同事重新平衡了花括号;即使除了一个压力很大的同事在 foo.h 中检查时没有发生任何事情,但未更改且不经意间更改了它的时间戳。”

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

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

于 2009-04-26T17:45:48.360 回答
-1

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

于 2009-04-26T17:02:02.617 回答