That's a lot to cover in one post. Basically, I want to talk some about these things together as they relate to my Kohi Game Engine Series on YouTube. In case you're not aware of it, it's a series I am hosting where I am building a game engine from the ground up completely in C, using Vulkan as a rendering backend (at least initially). I've answered some of what is discussed here on video, but I'll break this down a bit more in detail here. Disclaimer: Please note that everything contained here is my opinion. If you disagree, that's fine. I'm not saying you have to agree with me. It's not a "my way or the highway" situation here. I'm also not going to say I'm an expert on any of this. This is merely an overview of my findings after having worked with both languages over the years.
Why C instead of C++?
Simply put, I chose C for Kohi because of its overall simplicity as compared to C++. C++, especially in its recent iterations, has become bloated, extremely verbose and difficult to read and maintain in my opinion. Consider this bit of code from the C++ standard library:
auto start = std::chrono::steady_clock::now();
std::cout << "f(42) = " << fibonacci(42) << '\n';
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
This is actually shortened by the use of auto
here. It would be much longer otherwise, unless you do using namespace std
, but we all know why that's a bad idea. Granted, namespacing plays a big part in this case, but there's a lot more to this example that I take exception to than just verbosity. Namespacing can be useful, but can easily be replaced with just using decent naming in your C code.
First, the auto
keyword has never sat particularly well with me. Without an IDE to hover over it, there's no telling what its actual type is in many situations - especially if it returns from a function. This is really bad in situations such as a code review on GitLab, for example, where such functionality does not exist. The only place I ever used it in C++ was for lambdas, but that in and of itself is an entirely different conversation. A common argument for the use of auto
include that it makes refactoring easier, which I actually consider to be a drawback, not a benefit. For example, if you change the return type on a function from an int
to a float
and every call to said function stores the input in an auto
, the code will probably compile just fine. But, what if one of the uses of that return value actually expects that value to be an int
because of, say, some bitwise operations? A 1 in a float is not the same as a 1 in and int. I realize this is a contrived example, but many, many other issues could arise from this because it will typically compile just fine.
The next thing here is the commonly-overloaded <<
operator, in this case, used for streaming data to cout
. This to me is a huge issue with C++ that confuses many beginners. Don't get me wrong - operator overloads have their uses. For example, vector math can be greatly simplified with this, and is one of the main things I do miss about C++ when it comes to that. But this particular overload is terrible, especially when one learns its true intended purpose.
There's then the strange template syntax, which has become more common over the years. Templates in C++ are again a wholly different topic of their own, but suffice it to say I don't use them even when I do use C++. The only exception to this would be for container classes, but again I have ways of accomplishing this in C, so I don't really need C++ for that either.
What about OOP?
The main advantage C++ has over C is the easier implementation of Object-Oriented Programming, or OOP. I won't go into great detail about what OOP is here, since if you're reading this you likely already know. Suffice it to say that, for the most part, it involves making everything into classes and, by extension, objects (or instances of those classes). It focuses on abstraction by encapsulation (that is, hiding layers of logic and/or data within classes or class member functions). OOP has it's place, but for game development it's not always as useful as most believe it to be. Ofttimes I find it gets in the way far more than it actually helps.
I've found myself over the years leaning toward a more procedural approach, which both simplifies code and, in many cases, reduces the amount of code written to achieve the same thing. The implicit this
pointer can be nice sometimes, but I find most of the time it isn't needed. In the cases where it might be, it's not that difficult to work around by just passing a struct pointer to a function. If I really need inheritance for something (90% of the time, I don't), I can make my own sort-of v-table or just use function pointers. That's all C++ does under the hood anyway.
Yeah, but... raw pointers?
Speaking of under-the-hood, I hear so much talk about how using raw pointers is bad and dangerous. Well, it certainly can be dangerous if you do not know what you are doing, or write do not take care to write code properly of course. Folks forget, though, that using these newfangled smart pointers have an overhead both in memory and performance because (depending on the type) they must maintain a reference count or destroy themselves when going out of scope (again, viewed as a positive by many, but there are cases where it might not always be the right way to handle it).
This is very negligible of course on a small scale, but for performance-critical software like game engines it can add up fast, particularly on memory-starved platforms like game consoles or worse, mobile devices. Garbage-collected languages are even worse about this, saying nothing of course about memory fragmentation. Raw pointers are simply faster and lighter - and this counts in game development. They are also dead simple - just remember to have a free()
for every malloc()
, or a delete
for every new
if using C++ and you will never have an issue. If you are writing a desktop application or business software, then use smart pointers if you wish. Games? I say stick with simple, raw pointers.
Of course, it does needs be said that one should only dynamically allocate when absolutely needed, but try to keep on the stack whenever possible.
Wrap-Up
These are all conclusions I have come to over time, some on my own, some by listening to talks, doing research, or seeing how others work. There are no novel concepts provided here, as I am certainly not the first to have these preferences.
One more thing to note is that for a long time, because of all these things, I was programming in what I called "C++ Lite", which was basically C with only a few C++ language features being used. Specifically nullptr
, constructors/destructors on occasion, the occasional template for a container, and operator overloads - all features which I eventually decided I could live without. Thus, I made the jump to C and never looked back.
Make sure to check out the Kohi Game Engine Series if you haven't already, and I'll see you over there!