谷歌风格指南(前向声明章节)

9

前言

Google风格指南包括一份关于前向声明的缺点列表。

  1. 前向声明可能会隐藏依赖项,导致用户代码在头文件更改时跳过必要的重新编译。

  2. 后续对库的更改可能会破坏前向声明。函数和模板的前向声明可能会阻止头文件所有者对其API进行否则兼容的更改,例如扩展参数类型、添加带有默认值的模板参数或迁移到新命名空间。

  3. 从命名空间std::中前向声明符号会产生未定义行为。

  4. 很难确定是需要前向声明还是完整的#include。用前向声明替换#include可能会悄悄改变代码的含义:

代码:

  // b.h:
  struct B {};
  struct D : B {};

  // good_user.cc:
  #include "b.h"
  void f(B*);
  void f(void*);
  void test(D* x) { f(x); }  // calls f(B*)
 

如果将#include替换为B和D的前置声明,test()将调用f(void*)。
从头文件中向前声明多个符号可能比简单地#include头文件更冗长。
构造代码以启用前置声明(例如使用指针成员而不是对象成员)可能会使代码变得更加缓慢且更加复杂。
问题:
我特别关注第一点,因为我想不出任何情况,在这种情况下前置声明会跳过必要的重新编译。有人可以告诉我这怎么会发生吗?还是这是Google代码库固有的问题?
由于这是清单上的第一点,因此似乎非常重要。

2
对我来说,他们似乎将前向声明与提供自己的函数和类型声明混淆了。 - Barmar
@Barmar 对我来说也是这样。 - user4290866
由于包含头文件是提供前向声明的机制,@Barmar无疑是正确的。 - William Pursell
简单来说,只有在需要允许循环依赖时才真正需要前向声明,而这些声明通常应该在头文件中。在调用代码中使用前向声明是很少有借口的。 - Barmar
看起来很明显,这个列表的组织方式很令人困惑。 - Barmar
显示剩余4条评论
2个回答

2

我无法想象出一个场景,在这个场景中,前向声明会在头文件更改时跳过必要的重新编译。

我认为这也有点不清楚,可能需要用更清晰的措辞来表达。

依赖关系“隐藏”是什么意思?

假设您的文件main.cc需要header.h才能正确构建。

  • 如果main.cc包含header.h,那么这是一个直接依赖项。

  • 如果main.cc包含lib.h,然后lib.h包含header.h,那么这是一个间接依赖项。

  • 如果main.cc以某种方式依赖于lib.h,但如果不包括lib.h就不会生成构建错误,那么我可能将其称为隐藏依赖项。

然而,我不认为“隐藏”这个词是一个常见的术语,因此我同意措辞可以得到改进或扩展。

这是如何发生的?

我有main.clib.htypes.h

这是main.c

#include "lib.h"
void test(D* x) { f(x); }

这里是 lib.h 文件:

#include "types.h"
void f(B*);
void f(void*);

这里是types.h文件:

struct B {};
struct D : B {};

现在,main.cc 依赖于 types.h 以生成正确的代码。然而,main.cc 只直接依赖于 lib.h,它对 types.h 有一个隐藏的依赖关系。如果我在 lib.h 中使用前向声明,则会破坏 main.cc。但是,main.cc 仍然编译成功!原因是 main.cc 没有包含 types.h,即使其依赖于 types.h 中的声明。前向声明使得这种情况成为可能。

1
是的,但由于它没有声明任何“A”对象,如果您不重新编译,就不应该有问题。 - Barmar
1
好的,所以它重新编译了 main.cc,尽管实际上并不需要。该指南暗示重新编译是必要的。 - Barmar
1
很好的观点。但是指南中所说的“依赖项”是什么?如果有任何需要重新编译的真正依赖项,没有头文件代码应该无法编译。 - Barmar
1
@DietrichEpp 我认为关于“为什么在这种情况下需要重新编译”的推理也应该是答案的一部分。这是我至少特别不清楚的部分。 - k.v.
1
@Enigma:Google采用共享缓存和大规模的工作池来解决这个问题,因此即使是大型构建也非常快速。这里的void *只是我能想到的最简单的说明方式。真正被说明的问题是如果一个类被前向声明,不同的重载将被调用,而且这种情况也会发生在隐式构造函数或转换运算符等情况下。void *的示例只是我能想到的最简单、最短的一个。 - Dietrich Epp
显示剩余12条评论

1
我特别关注第一点,因为我无法想象出在头文件更改时使用前向声明会跳过必要的重新编译的情况。有人能告诉我这是如何发生的吗?
它会发生,因为依赖跟踪器无法推断出使用类的前向声明时定义类的头文件中的某些内容已更改。但是,在大多数情况下,这本质上并没有什么错误。
或者这是Google代码库固有的东西吗?
关于D的发布的代码块是有意义的。如果您不#include定义D的头文件,而只提供了前向声明,则对f(x)的调用将解析为f(void *),这不是您想要的。
在我看来,为了考虑以上用例而避免使用前向声明,而选择#include头文件是一个非常昂贵的代价。但是,如果您拥有足够的硬件/软件资源以使#include头文件的成本不是因素,我可以理解如何证明这样的建议。

令人困惑的是,这被提供作为第4点的示例而不是第1点。 - Barmar
@Barmar,确实。我猜有人在某个时候被第4点咬了一口,并决定由于引入的错误带来的痛苦/成本太大,因此决定通过支付额外编译时间的代价来完全避免它。 - R Sahu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接