This is still the header! Main site

On C++ headers, part 1: forward declarations

2024/09/22

Header files in C++ are one of those mildly annoying things that come with working with a programming language from the 80s that does not happen to be Common Lisp. Namely, whenever you are writing a class, you need to put the class definition in a header file:


// water_kettle.hpp
class WaterKettle {
public:
  WaterKettle();
  void start();
  int currentTemp();
private:
  Water* current_contents_;
};
          

... then copy all your method declarations from the class into C++ file:


// water_kettle.cpp

WaterKettle::WaterKettle() {
}

void WaterKettle::start() {
}

int WaterKettle::currentTemp() {
}

... all this before even starting to write a single line of actual code. Afterwards, if you want to modify a method signature, you need to change it identically in these two locations, assuming you want your code to actually compile.

Compare that to Java... or any other more modern language, where you can just write out your class once.

This seems like a silly relic from times when compilers couldn't afford reading files more than one time. (Maybe because they were on actual punch cards?)

And yet... sometimes, these things actually make sense.

This is Part 1 of... a hopefully longer series on how to make C++ somewhat less terrible than it typically is.

Forward declarations

You might have noticed that our WaterKettle class contains a pointer to the water in it. Does that mean that the compiler now needs to know what Water is exactly?

In Java (or many other more modern programming languages), the answer is "mostly yes". This makes your dependencies look like a tree. First, you compile classes that are basic types, and end with classes that depend on many others.

In C++ though, you can just say


// water_kettle.hpp
class WaterKettle;

class WaterKettle {
public:
  // (...)
private:
  Water* current_contents_;
};
          

This will make the compiler aware of water being some class that can be pointed to, without it having to read up on everything about it. It doesn't really know how big an instance is or what methods you can call on it, but this is fine. After all, the only thing we need to know currently is how to allocate a pointer to it in this class, which will take 8 bytes (on 64 bit architectures), no matter what.

Later, whenever you want to do something to it, you can include the actual header file containing the Water class definition. The nice thing about this is that this way you do not add a dependency to whoever is trying to use WaterKettle: the compiler only needs to digest water.hpp when it's looking at water_kettle.cpp, not when compiling complete_kitchen_with_kettle_and_sink.cpp.

This has... no obvious benefits when your project consists of a grand total of 3 classes (in this case, you like having to recompile everything whenever you change a header, giving you a greater sense of accomplishment). Once the project grows though, it's very useful for compilation speed.