9

Preface

The Google Style Guide includes a list of disadvantages of forward declaration

  1. Forward declarations can hide a dependency, allowing user code to skip necessary recompilation when headers change.

  2. A forward declaration may be broken by subsequent changes to the library. Forward declarations of functions and templates can prevent the header owners from making otherwise-compatible changes to their APIs, such as widening a parameter type, adding a template parameter with a default value, or migrating to a new namespace.

  3. Forward declaring symbols from namespace std:: yields undefined behavior.

  4. It can be difficult to determine whether a forward declaration or a full #include is needed. Replacing an #include with a forward declaration can silently change the meaning of code:

Code:

  // 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*)
 

If the #include was replaced with forward decls for B and D, test() would call f(void*).

  1. Forward declaring multiple symbols from a header can be more verbose than simply #includeing the header.

  2. Structuring code to enable forward declarations (e.g. using pointer members instead of object members) can make the code slower and more complex.

Question

I am particulary interested in the first point, as I cannot come up with a single scenario, where a forward decleration would skip necessary recompilation when headers change. Can anybody tell me how this can happen? Or is this something intrinsic to the google code base?

As this is the first point on the list it also seems rather important.

Community
  • 1
  • 1
  • 2
    It sounds to me like they're confusing forward declarations with providing your own declarations of functions and types. – Barmar Sep 17 '18 at 21:54
  • @Barmar So does it to me. –  Sep 17 '18 at 21:55
  • Since including a header file is a mechanism for providing forward declarations, @Barmar is undoubtedly correct. – William Pursell Sep 17 '18 at 22:10
  • @WilliamPursell Some of the guidelines are talking about forward declarations of incomplete types, which can be used when you just need a pointer. The header generally provides the complete declaration. – Barmar Sep 17 '18 at 22:12
  • Simply put, the only time forward declarations are really needed is to allow for circular dependencies, and these should usually be in the header files themselves. There's little excuse for forward declarations in calling code. – Barmar Sep 17 '18 at 22:18
  • @Barmar: The forward declaration is not enough to do overload resolution in all cases. – Dietrich Epp Sep 17 '18 at 22:28
  • @DietrichEpp I understand, that's what the example below point #4 shows. – Barmar Sep 17 '18 at 23:37
  • What seems to be clear is that the way this list is organized is confusing. – Barmar Sep 17 '18 at 23:38
  • The wording is appalling. User code doesn't skip compilations. Build tools skip compilations. – user207421 Jan 31 '20 at 03:12

2 Answers2

2

I cannot come up with a single scenario, where a forward declaration would skip necessary recompilation when headers change.

I think this is a bit unclear too, and perhaps could be worded a little more clearly.

What could it mean for a dependency to be hidden?

Let’s say your file main.cc needs header.h in order to be built correctly.

  • If main.cc includes header.h, then this is a direct dependency.

  • If main.cc includes lib.h, and then lib.h includes header.h, then this is an indirect dependency.

  • If main.cc somehow depends on lib.h but does not generate a build error if lib.h is not included, then I might call this a hidden dependency.

I don’t think the word hidden is a commonplace term for this, however, so I agree that the wording could be refined or expanded.

How does this happen?

I have main.c, lib.h, and types.h.

Here is main.c:

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

Here is lib.h:

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

Here is types.h:

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

Now, main.cc depends on types.h in order to generate the correct code. However, main.cc only has a direct dependency on lib.h, it has a hidden dependency on types.h. If I use forward declarations in lib.h, then this breaks main.cc. And yet main.cc still compiles!

And the reason that main.cc breaks is because it does not include types.h, even though main.cc depends on the declarations in types.h. Forward declarations make this possible.

Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • Why is this a problem? Isn't there a requirement that the format of a pointer isn't dependent on the layout of the type it points to? – Barmar Sep 17 '18 at 21:54
  • @Barmar: Without a forward declaration, you must recompile `main.cc` because `types.h` has changed. – Dietrich Epp Sep 17 '18 at 21:55
  • 1
    True, but since it doesn't declare any `A` objects, there shouldn't be a problem if you don't recompile. – Barmar Sep 17 '18 at 21:56
  • @Barmar: Your build system is not that smart. All it knows is that `types.h` has changed. – Dietrich Epp Sep 17 '18 at 21:57
  • 1
    Right, so it recompiles `main.cc` even though it doesn't really need to. The guideline implies that recompiling is necessary. – Barmar Sep 17 '18 at 21:59
  • Or do they mean "otherwise necessary"? –  Sep 17 '18 at 21:59
  • @Barmar: Recompiling is necessary, if you want to have verifiable builds. – Dietrich Epp Sep 17 '18 at 22:01
  • @Barmar: Note that it in large code bases, it is universally considered a bad idea to let users choose which files to recompile. – Dietrich Epp Sep 17 '18 at 22:02
  • 1
    Good point. But what "dependency" is being hidden, as stated in the guideline? If there's any real dependency that requires recompilation, the code should fail to compile at all without the header. – Barmar Sep 17 '18 at 22:07
  • Unless it's like the example after #4. – Barmar Sep 17 '18 at 22:07
  • 1
    @DietrichEpp I think reasoning regarding "why recompilation is necessary in this case" should also be a part of an answer. That's the part which is especialy unclear at least for me – k.v. Sep 17 '18 at 22:07
  • @Barmar: There is a very real dependency… the code depends on `b.h` being included. The dependency is “hidden” because you don’t get an error message when `b.h` is not included, instead, you call the wrong overload. – Dietrich Epp Sep 17 '18 at 22:15
  • Seems to me this is a problem with void* not with forward declarations. If there is no other example, this rule seems silly. – Enigma22134 Feb 01 '20 at 16:33
  • Context: [a question of Enigma22134's](https://stackoverflow.com/questions/59996751/how-is-it-that-a-forward-declaration-can-hide-a-dependency-and-cause-user-code-t) has been closed as a dupe of this question. – John Bollinger Feb 01 '20 at 17:33
  • @Enigma: A lot of rules for large, old codebases seem silly when you think about them from the perspective of greenfield projects. – Dietrich Epp Feb 01 '20 at 17:48
  • @DietrichEpp Yes that is true. But I wouldn't consider the project I'm on greenfield. Even with build farms our current project still takes at least an 40-60minutes for a full compile; even with extensive usage of forward declarations. There are a few cases where we have wide shared headers and touching one of those causes at least a ~20 recompile. Without forward declarations it would seem that many more header's would turn into these kind of land minds. Perhaps not though. For clarity, I think your answer is great for this question. I just wonder if `void*` is the primary motivation. – Enigma22134 Feb 02 '20 at 21:10
  • 1
    @Enigma: Google addressed this problem with aggressive shared caches for build artifacts and a massive pool of workers, so even large builds are fairly fast. The `void *` here is just the simplest way I could think to illustrate. The real issue being illustrated is that different overrides get called if a class is forward declared, and this also happens with implicit constructors or conversion operators, etc. The example with `void *` is just the simplest, shortest one I could think of. – Dietrich Epp Feb 03 '20 at 13:59
1

I am particulary interested in the first point, as I cannot come up with a single scenario, where a forward decleration would skip necessary recompilation when headers change. Can anybody tell me how this can happen?

It will happen because dependency trackers cannot deduce that something in the header file that defines the class changed if you use a forward declaration of the class. However, there is nothing intrinsically wrong about that in most cases.

Or is this something intrinsic to the google code base?

The posted code block about D makes sense. If you don't #include the header that defines D but provide a mere forward declaration, the call to f(x) will resolve to f(void*), which is not what you want.

IMO, avoiding forward declarations in favor of #includeing the header files is a very expensive price to pay to account for just the above use case. However, if you have enough hardware/software resources that the cost of #includeing header files is not a factor, I can see how one could justify such a recommendation.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • What's confusing is that this is provided as an example for point #4 instead of point #1. – Barmar Sep 17 '18 at 22:17
  • @Barmar, indeed. My guess is that somebody got bit by #4 at some point and decided that pain/cost of a bug introduced by it is too much, and hence decided to avoid it altogether by paying the price of a bit of extra compilation time. – R Sahu Sep 17 '18 at 22:30