I'm a professional Rust programmer and I admit I'm a bit of an evangelist. So you can imagine my pain when my little sister with very little programming experience said she wanted me to teach her C++. I tried to explain that one day she'll be debugging segfaults with valgrind and looking back at this conversation as the place where it all went wrong, but she proved to be even more stubborn than me and insisted on learning a programming language people actually use. I hadn't written any C++ since making games with Cocos2D-X in high school, but I decided that my vague memory of the "rule of three" (or was it five? or zero??) and other such matters would be more than sufficient for the task. It turned out I had some learning to do too, but I was pleased to find out that there is a large opportunity for knowledge transfer. Nearly every C++ concept can be easily understood as "oh, it's like this thing in Rust". And while C++ has its warts, it's a beautiful language in its own way. I already knew this, but in re-learning C++ it became even more clear that to whatever extent Rust surpasses C++ (if you believe that it does), it's only because Rust was able to stand on C++'s shoulders. So we spend a couple weeks following an [OpenGL tutorial series from a youtuber named TheCherno](https://www.youtube.com/playlist?list=PLlrATfBNZ98foTJPJ_Ev03o2oq3-GGOS2). (The series is excellent by the way.) After two weeks, we had managed to get a single inert blue square on our screen, and I was beginning to be afraid that my sister would question whether this was really the best way to learn a game and make C++. So at that point I decided we should peel off from Mr. TheCherno's tutorials and start making the game in earnest. We started implementing gameplay features and factoring them out into a rudimentary "game engine", and I'm proud to say that we now have an actual game you can download and play (codenamed [SpaceBoom](https://github.com/Alexpop11/SpaceBoom)). Once things were generally split into an engine side and a gameplay logic side, she ended up taking the wheel on gameplay (implementing much of it completely on her own!) while I responded to her feature requests on the game engine side. ## The Good of C++ #### Freedom C++ is a very freeing language. It lets you write code now and fix it later. Is what you're trying to do a bad idea? That's up to you, not some nagging borrow checker or pompous language designers who think they know better than you. Go ahead and dereference that pointer, what's the worst that could happen? It crashes or does something weird? It's a videogame, man. And besides, you already made the decision to write C++, so clearly something being a bad idea hasn't stopped you before. I'm joking a bit there, but C++ seriously lets you do whatever you want. I ended up writing this horrible [template for memoizing types that can be shared but have RAII](https://github.com/Alexpop11/SpaceBoom/blob/master/OpenGL/src/WeakMemoizeConstructor.h), which let me quadruple our game's FPS with only very minimal changes to the gameplay code my sister was writing. We don't stress about passing floats to functions that expect integers (although eventually the compiler warnings build up and we get around to fixing them). And if we need a global variable, we just make it static first and ask questions later. I'm good enough at Rust that I've internalized the Rust ways of doing things and can sketch things quickly despite all that, but I don't feel envious of the alternate-timeline version of myself where I'm enlightening her about `once_cell` and `Arc<Refcell<_>>`. #### Expressiveness Object orientation is also super nice. I know everyone hates on object orientation, but it's extremely pleasant for writing games with. We make a `GameObject` class, then `GameObject`s that are squares constrained to a tile grid get a `SquareObject` subclass , then `SquareObject`s that move around get a `Character` subclass, and so on. Then all of these go in one giant `vector<unique_ptr<GameObject>>` and you just call `->update()` and `->render()` on each one and it calls the right thing and just works. I historically was a big user of the Rust game engine Bevy (see [[automated-testing-in-bevy]]), and there is a lot to like about Bevy. But Bevy is supposed to be the gold-standard of the easy-to-use ECS, and making a game the way we did in C++ is just way more fun and productive. It's kind of a shame that using a real game engine ends up being a worse developer experience than some C++ my sister and I cooked up in a few weeks. (Again, neither of us even really knew C++ when we started.) It's not a fair comparison, since Bevy sacrifices some developer productivity for a lot of performance and is rapidly improving. But still, it's to C++'s credit that we were able to quickly get something with a great developer experience working. #### Debugging I really love the debugging story in Visual studio and VSCode. I don't know why but it feels like this just doesn't exist at this level for Rust. Even if it's possible, it's for some reason never been what I reach to. I always run my rust projects with Cargo or Bazel, and this loses something compared to being able to press a button in your IDE and just click to add breakpoints and whatnot. In my rust career I've used a debugger maybe once, everything else has been printf debugging. #### Modern C++ In C++, there are a lot of old features that you should generally not use over the "modern C++" equivalents unless you have a specific reason to. (`char *`, arrays, pointers, malloc/free, NULL, etc.) In a way it's like this classic image: ![[javascript_the_good_parts.png]] The upside is that modern C++ is actually very nice. Especially `unique_ptr` , `shared_ptr`, and `std::array`. The result of using these is that you mostly don't worry about memory safety. Of course, we did run into a few segfaults anyway in the course of developing the game, but they were always easy to fix. I do attribute some of this success to my rust experience though. We have a vector of `GameObject`s and we wanted references to them to be valid across frames, so we wrapped them all in `unique_ptr` (allowing us to append to the vector without risk of invalidating the references). The fact that references are invalidated when you append to a vector is obvious if you know how vectors are implemented, but I could easily see a newbie to this type of programming running into that issue. ## The bad of C++ C++ has the unfortunate but also awesome position of being an hierarchy of changes on a language originally developed in 1973, very rarely breaking backwards compatibility. This is a huge technical feat and I don't think it's fair to hold this against it. It often makes things a bit confusing, but it's preferable to the alternative. However I do think there are some unforced errors in C++ that maybe could still be corrected without causing mass chaos. #### Building is not as easy The lack of a standardized build system is really frustrating. She uses Windows and I'm on my Macbook, so at some point we switched from using straight up Visual Studio to cmake. cmake is a great piece of engineering, and maybe I just don't know how to use it, but the user experience of using it with Visual Studio is not great compared to Cargo. Visual Studio has a cmake mode that's works okay, but it's clearly second-class compared to using the real Visual Studio build system, and we ran into a ton of bugs that forced us to close and reopen it regularly (and in one case couldn't be resolved without a restart). Obviously Microsoft products being buggy isn't exactly C++'s fault and I can live with cmake. But it would be nice if cmake at least made easy things easy and tried to avoid "works on my machine" issues. On the upside, cmake has a great VSCode extension that is quite user friendly in my limited experience. #### Package management is not as easy Edit: Some people have told me about conan and vcpkg, which seems like they solve a lot of the problems mentioned in this section. However, I've left it in for posterity. The lack of a package manager in cmake is also a huge time waster. It has been a huge source of "works on my machine"-type issues. It seems like C++ developers like to install packages globally and then have their build system somehow find them and link them. But what will happen is that I'll try to do something on my mac where I apparently already have the library, then my sister will try it and she won't and it won't build. I've been working around this by just building everything from source as much as possible. The workflow for building library dependencies from source seems to be that you add the repository as a git submodule[^1], set up add_subdirectory and include_directories in cmake, then add the library in target_link_libraries. It's not always as easy as it sounds though, since some projects like glew aren't designed to be used as git submodules so you just actually dump a ton of their code directly into your repo, and knowing what library to use in target_link_libraries wasn't obvious to me for some reason (I wasted a ton of time with glew before realizing that I had to link with glew_s and not glew). Not to mention that most libraries we used seem to have a cmake folder with a readme that basically says "hey, some rando added this via pull request and I don't really know if it works but you can try it if you want". And if your dependencies have other dependencies I'm still not sure how that works. Overall it's a much more complicated experience than running `cargo add $CRATE_NAME`. Docs also seem to tend to be a bit harder to find, maybe because they aren't autogenerated. #### The error messages The error messages of C++ (both with Clang and Visual Studio) are also really rough compared to Rust's. I'd guess that the error messages are my sister's biggest impediment to learning C++ outside of the build system situation. This is what happens when I change the function signature in the header but not in the .cpp file or vice versa. ``` FAILED: OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o /usr/bin/clang++ -DGLEW_STATIC -I~/coding/SpaceBoom/OpenGL/vendor/openal-soft/include -I~/coding/SpaceBoom/OpenGL/src -I~/coding/SpaceBoom/OpenGL/vendor/glfw/include -I~/coding/SpaceBoom/OpenGL/vendor/glew/include -I~/coding/SpaceBoom/OpenGL/vendor/glm -I~/coding/SpaceBoom/OpenGL/vendor/soloud/include -I~/coding/SpaceBoom/OpenGL/vendor/xxHash/cmake_unofficial/.. -I~/coding/SpaceBoom/OpenGL/vendor/openal-soft/include/AL -iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk/System/Library/Frameworks -g -std=c++20 -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk -mmacosx-version-min=14.2 -pedantic -Wall -Wextra -Wcast-qual -Wdisabled-optimization -Winit-self -Wmissing-include-dirs -Wswitch-default -Wno-unused -Wno-cast-qual -Wno-unused-parameter -MD -MT OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o -MF OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o.d -o OpenGL/CMakeFiles/SpaceBoom.dir/src/Renderer.cpp.o -c ~/coding/SpaceBoom/OpenGL/src/Renderer.cpp ~/coding/SpaceBoom/OpenGL/src/Renderer.cpp:18:30: error: out-of-line definition of 'ResPath' does not match any declaration in 'Renderer' const std::string& Renderer::ResPath(bool idk) { ``` This was a very common situation for my sister to run into and man this error just is not great at first. Once you know what all the terms mean, it's precise and straightforward, but imagine someone like my sister who barely knows the difference between a "declaration" and a "definition". It doesn't even directly say that the definition is in `Renderer.cpp` and the declarations is in `Renderer.h`. I know that having good error messages in all cases is probably not achievable given C++'s design, but this particular case and other "simple" ones seem pretty easy to improve. [^1]:Git submodules also seem like they barely work to me. Why are they not updated when I run `git pull`? Submodules are cool but it feels like they were designed for a different use case.