Quantcast
Channel: blog.chrisrussell.net » Projects
Viewing all articles
Browse latest Browse all 13

C/C++ Header Encapsulation

$
0
0

I wrote a nice detailed blog post about ten years ago detailing a systematic and sane way to ensure that your large project collection of C/C++ headers don’t get out of control. Unfortunately, it was published on a blog that no longer exists (I’ve got the database somewhere on a CD but it’s more trouble than it’s worth to go digging for it). It absolutely amazes me how much code exists that absolutely sucks because the authors simply don’t understand C/C++ header encapsulation (or worse just don’t care).

Nothing drives me more nuts than including a header in my code written by someone else and breaking the build. When the build breaks in a case like this one of two things is going on: either there’s an actual error in the header (e.g. a syntax error), or the header’s declaration(s) depend on declarations that you don’t have in scope. Typically it’s the later case as few programmers worth their salt will knowingly give you a header that won’t even compile for them.

So what’s the deal? The deal is typically that the header author declared some stuff in the header in terms of declarations that themselves are not declared in the header you’re including. In other words they took a dependency on declarations included in another header and assumed it was obvious enough that it wouldn’t ever cause anyone any problems.

Here’s a simple example that has some hidden subtleties:

// header_A.hpp
#pragma once
struct A { int a; };

// header_B.hpp (revision 1)
#pragma once
void Foo(A & a_);

Note that as written the function Foo declared in header_B.hpp takes as input a reference to an instance of struct A declared in header_A.hpp. This means that callers of function Foo cannot simply include header_B.hpp in their cpp file and call the function. Instead they must themselves track down the missing dependency and include header_A.hpp.

One might refer to the documentation for function Foo and see that it is declared in header_B.hpp and write a simple skeletal cpp like this to get started:

// client1.cpp (revision 1)
#include “header_B.hpp”
void TryOutFunctionFoo() { /*I’m going to make a call to Foo here and see what it does*/ }

This of course won’t compile because function Foo’s signature depends on struct A which is not in scope in client1.cpp. Where is it struct A declared? The documentation says that struct A is declared in header_A.hpp. Okay, let’s try this:

// client1.cpp (revision 2)
#include “header_B.hpp”
#include “header_A.hpp”
void TryOutFunctionFoo() { /*I’m going to make a call to Foo here and see what it does*/ }

Nope. Same compile error. The order that client1.cpp includes the headers matters so to get this to compile we need this:

// client1.cpp (revision 3)
#include “header_A.hpp”
#include “header_B.hpp”
void TryOutFunctionFoo() { /*I’m going to make a call to Foo here and see what it does*/ }

Okay, now that it compiles we can go ahead and create an instance of struct A, pass a reference to Foo and see what happens. But there’s obviously a problem with header_B.hpp because we all would expect revision 1 of client1.cpp to compile cleanly. Specifically, the author of header_B.hpp should have forward declared struct A:

// header_B.hpp (revision 2)
#pragma once
struct A; // forward declare struct A
void Foo(A & a_);

Or, at least that’s the conventional wisdom and few people will argue with you if you do just that. There are well documented rules for when one should forward declare and include a header in another header and these are widely accepted. Here are several articles that talk about these rules in detail:

In this example the programmer wishing to make a simple call to function Foo still needs to figure out which header struct A is declared in but because of the forward declaration of struct A in header_B.hpp, the order of include no longer matters (i.e all revisions of client1.cpp presented here now compile cleanly). So this is at least an improvement.

Now suppose we add another structure declaration to header_B.hpp as follows:

// header_B.hpp (revision 3)
#pragma once
// struct A; // forward declare struct A
#include “header_A.hpp”
void Foo(A & a_);
struct B : public A { int b; };

You’ll note that we commented out the forward declaration of struct A and replaced it with a nested include of header_A.hpp. This is actually required as the forward declaration will not suffice. The compiler needs to know the size of struct A in order to compile the declaration of struct B (see the links above for all the details – there’s a bit more to it than is presented here).

What’s interesting is that the programmer wanting to make a trivial test call to function Foo can now simply write:

// client2.cpp (revision 1)
#include “header_B.hpp”
void TryOutFunctionFoo() { A a; a.a = 0 ; Foo(a); }

… and it works. Likely they never even stopped to consider where struct A is declared because simply including header_B.hpp got the job done via the nested include of header_B.hpp.

Interestingly, if we had nest-included header_A.hpp in revision 2 of header_B.hpp instead of forward declaring struct A, the programmer writing client1.cpp would not have had to explicitly included header_A.hpp in the original example. For what it’s worth, I prefer nested includes over forward declaration because:

  • I don’t have to waste time tracking down dependencies and explicitly including their declaration headers.
  • I like being able to look at the #include statements and seeing the filenames that I need to look at if I have further questions.

In practice, it seems to work well and I can’t think of any cases where nesting includes instead of forward declaring actually breaks anything. If such cases do exist, I’m sure someone will point it out to me :)

My preference aside, the real point of this article is to talk about the evil that is headers that impose include ordering constraints. Do what you want with regards to forward declarations vs. nested includes.

Headers that impose include order are evil because as projects grow the amount of time you can waste figuring out the magic order required to get the build off the floor can be large. This is actually way more important than it might appear in the trivial examples presented in this article.

How many times have you had to take over code written by someone else to fix a bug or add a feature only to discover that you need to do a little refactoring? Looks simple enough so you tell your boss it will take an hour. And then you get into it and discover that the declarations are so snarled up that if you touch anything the build is on the floor. Or you initiate a new project using a set of unfamiliar libraries and waste hours or days trying to figure out the magic order to include their headers just to get a completely empty project analogous to the simple test client to compile.

Here’s a really simple little trick that ensures that the headers you write do not impose hidden include ordering constraints:

// smoke_test_header_B.cpp
#include “header_B.hpp”

That’s it. Notice that smoke_test_header_B.cpp will not compile if anything declared in header_B.hpp depends on a declaration that has not been brought into scope by either a forward declaration or a nested include. For each header in your project create a simple smoke test wrapper and compile them all. If they don’t compile then you’ve got a problem. If they do, then anyone can include your header without regard to include order and this saves a ton of time.


Thanks to John Sheehan for pointing out that my first revision header example was flawed. I initially presented a simple example in which header_A.hpp declared a structure and header_B.hpp declared a function that took as input a reference to the structure. In this case a forward declaration of the structure declared in header_A.hpp in header_B.hpp would have sufficed. This prompted me to rewrite the example and brought me to the sub-point that I actually prefer nested includes vs. forward declarations – even in cases where a forward declaration is all that’s strictly necessary.



Viewing all articles
Browse latest Browse all 13

Trending Articles