9

前言

谷歌风格指南包括前向声明的缺点列表

  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 的前向decls,test() 将调用f(void*)。

  1. 从标头前向声明多个符号可能比简单地#includeing 标头更冗长。

  2. 构造代码以启用前向声明(例如,使用指针成员而不是对象成员)会使代码变得更慢和更复杂。

问题

我对第一点特别感兴趣,因为我无法想出一个场景,即当 headers 更改时,前向 decleration 会跳过必要的重新编译。谁能告诉我这是怎么发生的?或者这是谷歌代码库固有的东西?

由于这是列表中的第一点,因此它似乎也相当重要。

4

2 回答 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.c,lib.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,即使main.cc取决于types.h. 前向声明使这成为可能。

于 2018-09-17T21:52:15.647 回答
1

我对第一点特别感兴趣,因为我无法想出一个场景,即当标头更改时,前向删除会跳过必要的重新编译。谁能告诉我这是怎么发生的?

这会发生,因为如果您使用类的前向声明,依赖跟踪器无法推断出定义类的头文件中的某些内容已更改。但是,在大多数情况下,这并没有本质上的错误。

或者这是谷歌代码库固有的东西?

发布的代码块D是有道理的。如果您没有#include定义D但仅提供前向声明的标头,则调用f(x)将解析为f(void*),这不是您想要的。

IMO,避免前向声明以支持#include头文件是仅考虑上述用例的非常昂贵的代价。但是,如果您有足够的硬件/软件资源,#include编译头文件的成本不是一个因素,我可以看到如何证明这样的建议是合理的。

于 2018-09-17T22:10:13.397 回答