I’ve talked about C++ header files before. Like I said earlier in this series, the C language was designed in an age where memory was scarce and it wasn’t feasible for the compiler to hold your entire codebase in memory at once. So projects end up broken up into many different files.
The file marine.c needs to refer to the code in weapons.c, and vehicles.c. Somehow the compiler needs to be aware of the contents of those other files without loading them entirely. So we have a header file, which lists the contents of the other files. It’s like an inventory list. When the compiler is working on marine.c, it loads the header file weapons.h. Then the compiler can say, “Oh, I don’t know what the code for WeaponReload () looks like, but according to the header file I can tell that the code exists. I’ll just keep compiling and trust that the code for WeaponReload () will show up later when I’m compiling some other file.” This way the compiler can know that WeaponReload () exists, and the typo WeaponRelaod () doesn’t, enabling it to catch your error.
Vexation #4: Maintaining Header Files is not Programming
People defend header files by pointing out how useful it is to have them. You can get an overview of the weapons code by glancing through weapons.h, rather than doing a full read of weapons.c. Of course, it’s always super-useful when other people do a bunch of work for you. The question isn’t whether or not they’re useful, but if it makes sense for programmers to create and maintain these things.
Header files are generated manually by the programmer. You write the code for WeaponReload (), and then you add the definition for WeaponReload () to the header file.
It’s the programmers job to keep Weapons.c and Weapons.h in sync. Changes to one might necessitate changes to another. The list is nice to have, but it’s not always useful, even though you always need to create it. Suddenly the programmer isn’t coding, they’re taking inventory and doing bookkeeping. For the computer.
Sometimes header files need to include other header files. Sometimes these dependencies can be circular. A needs B needs C needs A. So you need to arrange things so that files are included in the right order, and you need to add special qualifying lines to help the compiler along. You need to stop it from including the same file more than once so it doesn’t get trapped in a circular chain.
This is a horrendous system.
- It greatly inflates compile times. Cars don’t care about guns, but they do care about marines, and if you want to refer to marines then you need access to the data types that marines use. So compiling cars.c means parsing the guns.h header file. That header file might include another, and another, and pretty soon this simple game object is 50 lines of useful code that includes 5,000 lines of header files for the compiler to chew on. And since the compiler is invoked once for every file in the project, it will need to redundantly parse those 5,000 lines many, many times.
- It makes it annoying to add things to the project. If I add a backpack data structure and make it part of the space marine, then I have to go to every single file in the project that refers to the marine and add the line #include “backpack.h” to it. (Or include backpack.h within spacemarine.h, exacerbating to the problem above.)
- Files need to be compiled more often. Bullets don’t interact with backpacks, but because of the way header files connect to each other, making changes to the backpack header will force bullets.c to need a recompile.
- Changing the data structures might necessitate re-arranging all the header files. If I make a change that means bullets.h is needed before lootbox.h, then I’ll need to manually edit all the source files that refer to both.
C++ inherited all of this from C. It was introduced in 1985, which is unfortunate. If it had arrived just a few years later, it could have been constructed differently and taken advantage of the hardware gains to compile things on a project level rather than doing everything a single file at a timeAlthough to be honest, this would have been unlikely. Abandoning header files would have been a really big shift. For good or for ill, the prevailing assumption was that C++ ought to be able to parse all the extant C code. . As it stands, C++ wastes programmer time (which is and always has been precious) to save on memory (which is ridiculously plentiful at this point) during compile.
Coders will point out that there are ways to mitigate all of these annoyances: We have precompiled headers, header files that include other header files, incremental builds, and a bunch of other things to make this less of a chore. Making it less painful is nice, but this is a hassle that shouldn’t exist at all. Text files are trivial, programmer time is precious, and this busywork doesn’t make the program better. The programmer is literally doing all this work to make it easier for the compiler to sort through data. That’s like having a construction worker use a shovel to loosen up the soil before they use the bulldozer to dig the hole. Why is the human doing work to make things easier for the machine?! Even if it only takes the programmer fifteen seconds to sort out a header problem, that’s literally billions of times longer than it would have taken the computer to sort it out.
So much work has been put into making compilers help the programmer by optimizing the code. It will change loops, ignore unused code, and do a bunch of esoteric sorcery to shave off a few processor cycles here and there. But then we still have all these programmers playing this absurd header file sorting game like its 1972.
Header files aren’t a part of any modern language. Rust, Go, C#, Java, PHP, Swift, Python, Visual Basic, Ruby, and Perl all seem to get along just fine without burdening the programmer a bunch of inventory homework. Still, if you’re making a AAA game, then you want the power of C / C++. And if you want that power, then you need to endure the vexations of header files.
Before we cover the next vexation, let’s stop for an aside and talk about…
In C++, every line must end with a semicolon. This leads to the old programmer adage “A semicolon is mandatory everywhere it isn’t forbidden”. Semicolons have fallen out of favor in the last generation or so and I see a lot of people questioning why a language would have such a dumb feature.
All C family languages use them. So do Java, D, PHP, Perl, and Rust. But Python, Go, Groovy, Visual Basic, and Ruby don’t. Swift is an odd one, where semicolons are supported but optional. I get the sense that current-day attitudes among the younger coders lean away from semicolons.
I can understand why some people find them pointless. A semicolon is supposed to mark the end of a line.
a = a + 1; b = a + 2; c = a + b;
The compiler doesn’t care about whitespace, so you could format that code like this and it would be the same to the compiler:
a = a + 1; b = a + 2; c = a + b;
But don’t we already have a way to mark the end of a line? That’s what the return key is for! Why not mark the end of a line of code by actually ending the line of code?!
I can understand why people say this, but let me try to make a case for semicolons.
The Case for Semicolons
Because of the kind of work I do – procedural object generation – I sometimes wind up with a lot of really long lines. I’ll have a lot of short lines that only use the first 10 or 20 columns of horizontal space, and then a block of enormously long lines that extend far beyond 100.
Some of the code can get pretty complicated and hard to explain, but here’s a simplified example:
//Put the hat on the player's head. hat.MoveTo (Vector3D (player.avatar.position.x, player.avatar.y + player.height, player.avatar.position.z));
We’re trying to put a hat on a character’s head in 3D space. So we take the player’s originTypically, this is the point at the base of the model, even with the soles of the feet., add the player’s height to the Y component, and move the hat to the resulting location.
In my experience, most coding projects encourage you to keep individual lines of code under 80-ish characters long. Back in the old days, this was the limit of how much text you could fit on the screen horizontally. We’re obviously way beyond that now. I just tested it in my IDE, and even with a large font my 1080p monitor can easily fit ~150 characters on a line without needing to do any horizontal scrolling. However, this is still discouraged. As monitor resolutions have gone up, the preference seems to be for having more files open side-by-side. Rather than leaving this vast expanse of unused empty space on the right, why not split the window in half and show different files on the left and right sides?
The line of code above isn’t outrageously long. It’s only 108 characters, plus some indenting depending on how deeply this is nested. If a single line jutted out to column 108, most project managers would overlook it. But at some point, that line sticks out so far that you can’t see it without horizontal scrolling. Having bits of code hidden beyond the right edge of the screen is annoying and scrolling back and forth to read code sucks.
In a language with semicolons, I can take that single line of code and break it up like so:
hat.MoveTo (Vector3D (player.avatar.position.x, player.avatar.y + player.height, player.avatar.position.z));
To the compiler, that’s still a single line of code, because a line doesn’t end until it sees the semicolon. In a language without semicolons, you can’t do this. If a line is ridiculously long, you need to either break it up into a series of discrete operations or tolerate the horizontal scrolling.
You could shorten this line like so:
Vector3D hat_pos = player.avatar.position; hat_pos.y += player.height; hat.MoveTo (hat_pos);
What I dislike is that I’m creating this extra variable. That variable will hang around as long as it’s in scope and there might be some overhead to creating itVariable constructors can often hide a bunch of operations from you, so there might be an unseen cost to creating a new variable. Although the one-line version might ALSO create a temporary variable. It’s complicated.. This variable isn’t needed by the code and it only exists for the purposes of layout. Making additional variables to solve formatting and spacing problems is like wearing oversized shoes because you like the extra spacing around the Nike logo.
Like I said above, the whole point of having the compiler ignore whitespace is so that we can format code in whatever way makes visual sense to the reader. Semicolons make it easier to do that by decoupling how we view the code from how the compiler interprets the code.
In Jai, Jon Blow has specifically said that he’s “agnostic” on semicolons. Last time I looked, Jai seems to use them, but I get the sense that he’s not attached to them and could easily abandon them if he found a good reason. Like Blow, I don’t think this issue is that important. It’s an interesting thing to think about – particularly if you’re considering making a new language – but it doesn’t need to be part of the programming holy wars. I prefer them, but I’m perfectly happy using a language that doesn’t have them. I’m happy as long as the language has one of these two ideas:
- The line doesn’t end until you enter a semicolon.
- The line ends if you hit the return key, EXCEPT you can use some special symbol to indicate that this line is continued below.
As long as I have the option to have one logical line span multiple lines of text, then I don’t really care which system we use. I’m partial to semicolons because I’ve been using them for 29 years, but there’s nothing wrong with the alternative.
 Although to be honest, this would have been unlikely. Abandoning header files would have been a really big shift. For good or for ill, the prevailing assumption was that C++ ought to be able to parse all the extant C code.
 Typically, this is the point at the base of the model, even with the soles of the feet.
 Variable constructors can often hide a bunch of operations from you, so there might be an unseen cost to creating a new variable. Although the one-line version might ALSO create a temporary variable. It’s complicated.
In Defense of Crunch
Crunch-mode game development isn't good, but sometimes it happens for good reasons.
Trashing the Heap
What does it mean when a program crashes, and why does it happen?
id Software Coding Style
When the source code for Doom 3 was released, we got a look at some of the style conventions used by the developers. Here I analyze this style and explain what it all means.
Why The Christmas Shopping Season is Worse Every Year
Everyone hates Black Friday sales. Even retailers! So why does it exist?
Game at the Bottom
Why spend millions on visuals that are just a distraction from the REAL game of hotbar-watching?