[英]Is it worth forward-declaring library classes?
我刚开始学习Qt,使用他们的教程。 我目前正在使用教程7,我们已经制作了一个新的LCDRange类。 LCDRange(.cpp文件)的实现使用Qt QSlider类,因此在.cpp文件中是
#include <QSlider>
但在标题中是一个前向声明:
class QSlider;
根据Qt,
这是另一个经典的技巧,但经常使用的技巧要少得多。 因为我们在类的接口中不需要QSlider,所以只在实现中,我们在头文件中使用类的前向声明,并在.cpp文件中包含QSlider的头文件。
这使得大项目的编译速度更快,因为编译器通常花费大部分时间来解析头文件,而不是实际的源代码。 仅这一技巧通常可以将编辑速度提高两倍或更多。
这值得吗? 这似乎是有意义的,但是还有一件事要跟踪 - 我觉得将所有内容都包含在头文件中要简单得多。
绝对。 C / C ++构建模型是... ahem ...一个时代错误(说最好的)。 对于大型项目,它将成为一个严肃的PITA。
正如尼尔正确地指出,这不应该是你设计类的默认的态度,不走出自己的路,除非你真的需要。
Breaking Circular包括引用是您必须使用前向声明的一个原因。
// 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分钟构建的几秒(或更少)中削减。
我用它所有的时间。 我的规则是,如果它不需要标题,那么我提出了一个前向声明( “如果必须,请使用标题,如果可以,请使用前向声明” )。 唯一糟糕的是我需要知道如何声明类(struct / class,如果它是模板,我需要它的参数,......)。 但在绝大多数时候,它只是归结为"class Slider;"
或者其他什么。 如果某些东西需要更多的麻烦才能被宣布,那么总是可以声明一个特殊的前向声明标题,就像标准对iosfwd
。
不包括头文件不仅会减少编译时间,还会避免污染命名空间。 包括标题在内的文件会感谢您尽可能少地包含,以便他们可以继续使用干净的环境。
这是一个粗略的计划:
/* --- --- --- Y.hpp */
class X;
class Y {
X *x;
};
/* --- --- --- Y.cpp */
#include <x.hpp>
#include <y.hpp>
...
有一些智能指针专门设计用于指向不完整类型的指针。 一个众所周知的是boost::shared_ptr
。
标准库对标准头文件<iosfwd>
中的某些iostream类执行此操作。 但是,它不是一种普遍适用的技术 - 请注意,其他标准库类型没有这样的标题,并且它不应该(IMHO)是您设计类层次结构的默认方法。
虽然这对于程序员来说是最受欢迎的“优化”,但我怀疑,与大多数优化一样,他们中很少有人在有或没有这样的声明的情况下实际构建他们的项目。 我在这方面的有限实验表明,在现代编译器中使用预编译的头文件使其不必要。
对于较大的项目,即使是具有谨慎管理的依赖项的项目,编译时间也存在巨大差异。 你最好养成向前声明并尽可能多地保留头文件的习惯,因为在很多使用C ++的软件商店都需要它。 你在标准头文件中看不到这么多的原因是因为那些大量使用模板,此时声明变得很难。 对于MSVC,您可以使用/ P来查看在实际编译之前预处理文件的外观。 如果你没有在你的项目中做过任何前瞻声明,那么看看需要做多少额外的处理可能是一个有趣的经验。
一般来说,没有。
我曾经尽可能多地向前宣布,但不再。
就Qt而言,您可能会注意到有一个<QtGui>
包含文件将<QtGui>
所有GUI小部件。 此外,还有<QtCore>
, <QtWebKit>
, <QtNetwork>
等。每个模块都有一个头文件。 似乎Qt团队认为这也是首选方法。 他们在模块文档中这么说。
没错,编译时间可能会增加。 但根据我的经验,它并没有那么多。 如果是这样,使用预编译头将是下一步。
当你写...
...从而指示一个传统的构建系统“任何时候在库文件foo.h中有任何改变,丢弃这个编译单元并重建它,即使foo.h发生的所有事情都是添加注释,或者对foo.h包含的某个文件添加注释;即使所有发生的事情都是一些超级挑剔的同事重新平衡花括号;即使没有发生任何其他事情,除了受到压力的同事检查foo.h不变且无意中改变了它的时间戳。“
你为什么要发出这样的命令? 库标题,因为它们通常比应用程序标题具有更多的人类读者,对于对二进制文件没有影响的更改具有特殊的漏洞,例如改进的函数和参数文档或版本号或版权日期的颠覆。
C ++规则允许在编译单元中的任何一点(与结构或类不同 )重新打开命名空间 ,以支持前向声明。
前向声明对于打破循环依赖非常有用,有时可以使用您自己的代码,但将它们与库代码一起使用可能会破坏另一个平台上的程序或其他版本的库(即使你的代码,如果你不够小心)。 恕我直言不值得。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.