18 December 2022

The surprisingly complex world of C++ build systems

TL;DR: I’ve set up a template repository with my current setup over on GitHub. It uses Bazel to build a single binary, and is set up with a test framework (googletest). If you use VSCode, it also includes a debug config and should automatically set up IntelliSense (this apparently doesn’t come for free with C++).

I’ve been learning C++ recently. I’ve realized that a lot of programming constructs/ abstractions in modern languages like Rust just don’t make sense to me. I think this stems primarily from my inexperience. As such, I’ve never really felt productive in Rust despite trying on-and-off for the last couple of years — nothing to show for it (a burnout may have something to do with this too 😬). I cannot use abstractions if I cannot justify why they exist. This is just one of the ways my brain fucks me over every day 🫠.

So I decided to give up and just learn the language Rust (and others) aim to replace, get some first-hand experience, you know. So far, I’m kinda liking C++. I may be writing unsafe code that will burn everything in its path but everything makes sense to me and I’m having a lot of fun 🤷‍♂️.

I’ve discovered that there are a whole bunch of ways to compile your code to a binary. C++ has a non-conventional module system, in that it doesn’t have one at all. This means all files are their own units (translation units). Once you get used to this, you quickly realize that running the compiler directly (on macOS, this is clang) becomes unfeasible almost instantly. C++ doesn’t have a build system like newer languages so a lot of people have come up with ways to do this. I dug into a few of them last week.

An issue I had with a lot of them was that there was an actual learning curve to almost all of them. I did invest some time into learning a couple, but it was intentionally limited because I’m also learning C++, and I didn’t want to get bogged down, in the spirit of “doing exactly one thing at a time”

Another issue was that because there is no convention, all these build systems are naturally incompatible. This means that #includeing libraries that use one toolchain is hard/impossible if you use a different toolchain (assuming you you want static linking, which is likely). Fortunately the big players have solved this problem for the most part.

Speaking of libraries, there isn’t a package manager either, so you kinda have to pick a build system, even if it is just a bag of shell scripts. A lot of people do seem to be converging on vcpkg though.

Anyway, I expected this stuff from an “old” language. My reasons for learning C++ take this into account.

There are no doubt tons more. I’ve settled on Bazel for now because I like it the most.

It also looks like you do not get IntelliSense out-of-the-box for non-system libraries. This is to be expected because there is no module system! I did some research and there are ways to generate a file (compile_commands.json) that will let IDEs let you “jump-to-definition” at least, which is all I really need.

While I was at it, I also set up VSCode’s debugger. I’ve never used this in TypeScript because it used to be weird to do that with Node, but I want to get better at this.

Here’s a template GitHub repository I’ve set up that has all this: sdnts/cpp. This has a single Bazel workspace with a single package. Bazel does monorepos really well so it should be easy to add more packages if I ever need it. The template also includes an external library for unit tests (google/googletest), which seems popular with C++ devs. On VSCode, you’ll probably also want the C/C++ extension (just the single extension, no need to bloat your editor up with the extension pack). I hope to add a few GitHub Actions as well soon when I need them.