top | item 23202120

Modern C++ gamedev: thoughts and misconceptions

221 points| ingve | 5 years ago |vittorioromeo.info

217 comments

order
[+] PezzaDev|5 years ago|reply
After years of experience writing code for games, I find that the dumbest code is the best code. It doesn't matter if it's C# or C++, whenever I've used something like reactive extensions or template metaprogramming, it has been a terrible mistake every single time. Make your code simple, dumb and verbose all the time. Avoid using any complex abstractions or any overcomplicated syntactic sugar and you'll have a codebase that anyone can jump into and quickly be able to add features without introducing bugs (at least less likely). This matters more than anything else.
[+] microtherion|5 years ago|reply
> Make your code simple, dumb and verbose all the time

This is a frequently encountered argument, and sure, if you look at any single line, it looks very obvious what it does. But I would argue that verbosity and lack of abstraction has severe drawbacks for a programmer's ability to understand the overall codebase, and it is disastrous for long term maintainability.

You start out with 20 identical pieces of boilerplate code, and a few years later, you have 20 subtly different pieces of code. Good luck guessing whether the differences were intentional, or accidental. Good luck refactoring the code.

[+] lazypenguin|5 years ago|reply
Matches my experience as well in the hobby gamedev realm. I can give an anecdote, I’m involved in a “private server” dev community for an old niche MMO from 2003. There exists 3 codebases: first the original code written in a windows-style C++ with heavy use of inheritance, custom collections, Hungarian notation, etc. The second is a collaborative open source version that is written in a naive/basic C++ style (e.g. c w/ classes) and the third is a from-scratch reimplementation in modern C++ with all the bells and whistles, cmake, heavy use of templates, etc.

Despite the modern c++ version being the highest quality and the original windows-style version being the most fully featured, the vast majority of people use and prefer the rinky-dink basic c++ version. Simply for the reason that you don’t need to be a senior level dev to contribute meaningfully to the code.

[+] krapp|5 years ago|reply
>I find that the dumbest code is the best code

Not always, though.

See every bug and exploit with C arrays or pointers that exists because C devs think even minimal attempts at safety are too complicated or slow, or old-style PHP code that builds SQL queries out of printf strings directly from POST values, or probably countless other examples in other languages. C++ code that uses raw pointers instead of references or that uses std::vector but never actually bothers to bounds-check anything.

It's entirely possible for code to be too dumb for its own good.

[+] typon|5 years ago|reply
I wish this kind of thinking would die. 9 times out of 10 when I'm working on a large code base at (any company I've worked at so far), the main problem I have is with disorganized, messy code with poor abstractions, state everywhere, functions that are too long, functions that are too short, references to objects everywhere with no regard for lifetime and the list goes on and on. The times I have been stumped with clever syntax are few and far between. Almost never have I said "oh man I wish they didn't use a std algorithm/container here, makes the code more obfuscated!".

Yes I have seen cases where classes or functions are unnecessarily generic, adding templates when you only needed to support a specific type (YAGNI).

But in the end, most bad code I look at, I completely understand it's syntax. It's the semantics of this so called "dumb" code that prevent me from modifying it or fixing a bug in it for days until I actually understand the rats nest of ideas expressed in the code.

I think using features like using const as much as possible, preferring return by tuple rather than multiple in out parameters and a bunch of other modern C++ features more often than not make code bases simpler than the other way around.

[+] HelloNurse|5 years ago|reply
C++ fold expressions, the main C++ feature the article covers, are simple and dumb (at least, enough to use them) but not verbose, and definitely harder to get wrong than a much more verbose for loop.
[+] taneq|5 years ago|reply
As I've learned more about programming I've been leaning more and more in this direction too. So very much of the complexity and abstraction we build into software is gratuitous and unnecessary. The real art of writing software is writing simple, obvious code that also happens to be fast and efficient.
[+] pansa2|5 years ago|reply
> I find that the dumbest code is the best code.

This is the idea behind Golang, isn’t it? That everything should be written out explicitly and not hidden behind abstractions. Some people love that, others hate it.

[+] CamperBob2|5 years ago|reply
It's just so frustrating that they keep adding things that scratch no itch I've ever had as a programmer, while leaving out incredibly obvious things that would make my life easier and my code safer.

Named parameters, for instance. It's insane that function parameters still can't be specified in any order with name=value expressions. That would have saved numerous bugs over the years, but apparently I'm the only one who thinks so. When a language such as C++ is harder to use than Verilog, somebody has screwed up badly.

[+] RcouF1uZ4gsC|5 years ago|reply
> Avoid using any complex abstractions or any overcomplicated syntactic sugar and you'll have a codebase that anyone can jump into and quickly be able to add features without introducing bugs (at least less likely).

The thing is what is complex abstraction changes with time. At one time, floating point, especially transcendental functions would have been a complex abstraction. Functions at one time were a complex abstraction. Classes and polymorphism were a complex abstraction. Pointers are a complex abstraction for a lot of people. Linear algebra is a complex abstraction. Transforms are a complex abstraction.

Many times “complex abstraction” just means “an abstraction I am not familiar with”.

Back in the 80’s most games were written in assembler. I am sure many people thought that C was a complex abstraction. I mean, instead of just doing a jmp to a location, now you had a stack and a calling convention and which registers to save and restore...

Since “complex abstraction” is often code for “unfamiliarity”, education, like what the original article is doing is very helpful in moving the state of the art forward. As people become familiar with new abstractions, that becomes the new baseline for “simple” code.

[+] meheleventyone|5 years ago|reply
Yup, I think everyone goes through a phase of enjoying creating ‘elegant’ abstractions but you steadily learn nuts and bolts is way more preferable. That’s why I’m way more excited about Zig as a language to write games with than modern C++.
[+] badsectoracula|5 years ago|reply
I've worked on a bunch of engines, both big AAA and small indie scaled ones and i agree with you. It is actually from this experience that i have a hard dislike for C++'s "auto" outside of cases where you can't do otherwise - it makes the code you didn't write hard to understand exactly what is going on (and sometimes error prone). Sure IDEs can show you the type if you mouse over (at least some of them), but if the type is explicitly typed you do not need to do that and you can just read instead of pause, move the mouse over the auto, read the type, then move the mouse to the next (if any), etc. And that is assuming you are reading the code inside an IDE - it doesn't work if you are reading the code in a web-based code review tool that at best can show you syntax highlighting (and it is exactly in that environment where you want the code to be at its most understandable).

Now not all features are bad, lambdas are OK when used as local functions and can make the code more readable if the alternative is to define some static function outside the current method (e.g. you want to pass some custom filter or comparator). They can certainly be abused though, but it is one of those cases where their usefulness is greater than their abuses (and i can't say the same for "auto").

For the example given... it might be a bad example, but honestly i was looking at that code for a bit and i simply cannot read it - i do not understand what is going on just by reading the code, i'd need to run it in a debugger and go through it step by step (and i've actually written texture atlas packers before). It completely fails to sell me on the "fold expressions" and "parameter packs" and it certainly doesn't look at all "elegant" to me (but note that it might be that the example is awful, not the language feature itself).

And it did make me skim through the rest of the article though since after completely failing me on all fronts at the introduction bits, i couldn't get the feel that i have any common grounds with the author.

[+] pornel|5 years ago|reply
Nobody thinks "I hate having simple code, I'm going to replace it with a complex one!"

It's always "ugh, this code is a copypasta convoluted mess. I'm going to replace it with a simple solution".

[+] MacSystem|5 years ago|reply
That's why even using C# I wanted to have some of C's simplicity. Keep it simple. As the Da Vinci said, "simplicity is the ultimate sophistication"
[+] gintery|5 years ago|reply
Is the code really that complex? Overly verbose, maybe, but its really not that hard to follow even for someone who doesn't know much about C++ templates.
[+] papito|5 years ago|reply
Isn't that true for, like, any code? Don't turn your code into an academic exercise. You are not writing it for the compiler - you are writing it for me.

It's not that different from the English language either. Laying out your thoughts in a clear and structured way is the real skill. Start using words and expressions that I have to constantly google for, and I will hate you very quickly.

[+] vbezhenar|5 years ago|reply
I agree wholeheartedly. I spent so much time needlessly trying to be smart.
[+] otabdeveloper4|5 years ago|reply
Template metaprogramming is not 'smart'. In fact, it's the dumbest and simplest programming language, not counting esolangs.

You just need to know functional programming.

Know your tools, people.

[+] tonetheman|5 years ago|reply
This advice is true across all areas of programming.

I always say it as "write code that you could debug at 2:00 am drunk."

Be simple, do not be clever and be clear.

[+] Koshkin|5 years ago|reply
The tool is only as good as the one who uses it.
[+] eska|5 years ago|reply
Reading the twitter thread he mentioned in the introduction gave me a very bad impression of the author. He started a nonconstructive flamewar, called all constructive criticism misguided, and thereby shit on half a dozen video game development VETERANS. The arguments by Omar in particular are worth reading much more than this article.

Also, all his code examples are bad. No one would build a texture atlas the way he did for example, and in any practical algorithm one wouldn't be able to use folds like he did. So suddenly one would have to use something entirely else like a for loop or std::accumulate().

[+] Jasper_|5 years ago|reply
Yeah, the weird thing is that this is not an atlas packer! It's a very crude long texture with arbitrary bounds, and most GPUs have a maximum texture dimensions, so given enough particles this would have broke. Even a simple skyline algorithm to pack rectangles would have worked far better and it's hard to see how to make that into modern C++ like the author shows.
[+] 1tCKV3QfIo|5 years ago|reply
Bloomberg has a higher hiring bar than most, if not, all game studios. I like how you fallback on seniority while the video game industry isn't exactly known for top tier talent.
[+] winterismute|5 years ago|reply
While I do agree with various points in the article, it is kind of funny that the author works in the financial sector and has, apparently, no experience in working on big games devloped in long stretches by hundreds of people at the same time: for example, this article about "C++ and gamedev" shows examples from his own Quake VR codebase, an insanely cool but clearly one-man project started and brought forward by he himself alone...
[+] dkersten|5 years ago|reply
So? Doesn't mean people have to be toxic about a code snippet he posted because he found it interesting. Personally, I'm glad he posted about fold expressions, because I'm not up to date on C++17 yet and I found it quite interesting. He didn't deserve the replies he got and I feel that the only reason this article is about gamedev at all is because the gamedev community were the ones who jumped on him (and probably only because he mentioned his Quake VR project).
[+] humanrebar|5 years ago|reply
Does the author have no experience developing C++ on large teams or are you asserting that large team game dev is unique compared to other industries?
[+] sharpneli|5 years ago|reply
There was a good comparison about those floating around:

https://imgur.com/a/u1N4Fpy

For me the code on the right is far more readable and easier to understand.

[+] hhmc|5 years ago|reply
The code on the right feels in mildly bad faith to me.

  - consts are dropped
  - variable definitions are merged onto multiple lines
  - usefully named constants like `nPixels`, `nBytes` are elided
  - the `idx` lambda is inlined
The net effect, for an initial skim, is that the code on the right looks terser and simpler. But in reality many of the "short cuts" hurt the long term quality of the code.
[+] s9w|5 years ago|reply
I agree with the author on many things - parameter packs however.. I've looked at them several times. Even if you get a grasp on their syntax, I've almost never been in a situation where they could be used. As he correctly observes it's a compile time thing.

> In my particular scenario, all the texture file paths are hardcoded

That feels like a very unique case. Usually data is given in a vector or something and then you're out of luck anyways. Plus the syntax is very unintuitive (think about your colleagues), debuggability is zero.

Also the argument about constness feels a bit contrived. Sure with this you can write const and feel good, but the other version is hardly a bad nonconst. You can wrap things in functions or whatnot. The function seems to be a bit long anyways.

[+] saagarjha|5 years ago|reply
Parameter packs are quintessential compile time lists: you can pass them around, transform them, destructure them…they are really quite useful when you need to make sure certain things are done at compile time. For example, I recently used them heavily to generate code at compile time for a virtual machine I designed, all from a single instruction architecture that I encoded in the type system.
[+] dkersten|5 years ago|reply
> Even if you get a grasp on their syntax, I've almost never been in a situation where they could be used.

Parameter packs can make some ugly code substantially simpler, IMHO. For example, I contributed some changes to the Godot C++ bindings a couple of years ago that made a number of super common functions variadic instead of having to create and pass in collections (eg debug printing, specifying argument types when registering signals, stuff like that). While not a strictly necessary change, it makes the resulting code easier to read. Parameter packs allow this.

[+] leni536|5 years ago|reply
Variadic templates are essential for simple, argument forwarding template functions. std::vector::emplace_back is a very simple example, this kind of use comes up now and then during work.

[1] https://en.cppreference.com/w/cpp/container/vector/emplace_b...

edit: This also shows that you don't have to be able to write variadic templates to reap the benefits of it. You can enjoy the benefits while using a library that uses it.

[+] unchar1|5 years ago|reply
I find the twitter thread to be specially sad, since I see a lot of people genuinely try to explain that compile times/familiar syntax are more important to them, but the author seems adamant that they just don't know what modern tech is and they should learn the "right" approach.
[+] leni536|5 years ago|reply
I feel that most of the problems with debuggability is tooling (still). No, I do not want to debug the standard library most of the time, please let me optimize that, while I keep my part unoptimized.

Hopefully modules will allow mixed optimization levels for template headers.

Metaprogramming also lacks a good debugging story, but I would be happy to be proven wrong.

In my opinion these are not language problems, but tooling problems.

[+] Jare|5 years ago|reply
There are designs and usage choices that make the tooling problem orders of magnitude harder. You still have to use the tools you have, today, while the tools of tomorrow arrive. The debate is about what and why (some of) those choices are taken or rejected by different people.
[+] nobbis|5 years ago|reply
His personal blog post has eight obtrusive ads plus a Donate button (to an engineer in finance in London.)

I get the sense the author's deliberately stirring controversy.

[+] tomnj|5 years ago|reply
I’m often disappointed by how disrespectful people can be in programming discussions. It’s one thing to discuss tradeoffs but ridiculing peoples’ choices or speaking in absolutes is not helpful. There’s no One True Way to program.
[+] eska|5 years ago|reply
I always wondered whether it's programming in particular that attracts a lot of people that think in only black and white, or whether it's the same in other industries as well.
[+] saagarjha|5 years ago|reply
Really the “problem” here is that C++’s functional constructs will never allocate memory, giving them somewhat strange signatures that don’t really conduce themselves well to typical functional concepts. At compile time there is no such goal and as such some of these constructs can only be found there.
[+] lewis1028282|5 years ago|reply
That site is awful on mobile and filled with ads. Makes it impossible to read.
[+] pacificat0r|5 years ago|reply
He got upset when ppl with 15+ years of experience, working at dice or directors of tech at Activision pointed out that modern c++ is over complicated and they've been in that phase of liking abstractions but moved on. Funny he blanked the names of those dudes and some other milder replies.

Then he wrote a blog post on it.

[+] DethNinja|5 years ago|reply
I would personally use for loop without size_t. It is short and simple and works as intended, anyone can understand it.
[+] jdmoreira|5 years ago|reply
I stopped reading at "I would like this to happen at runtime".

I got burnt so many times by runtime craziness already that the only thing I want happening at runtime is trivial code running from top to bottom.

But if you enjoy runtime hacks, more power to you! You must be a much better developer than me.

[+] theincredulousk|5 years ago|reply
I don’t think this really covers the whole topic, but yeah the Twitter thread is toxic in the exact way most threads are tbh...

I’ve been using C++ for about 15 years now, and also tbh, and dislike just about everything beyond 11-14. The cognitive load reading code has just skyrocketed. The simple basis of the issue was when the meaning of existing syntax elements being “overloaded”. &, &&, [] in my mind. It is not complicated or profound reasoning - simple as now you need to decipher more context to understand what is being expressed. I’ve been materially disappointed to spend 5-10 minutes reading a small bit of unfamiliar code only to learn that what it ultimately expressed was truly trivial. It just makes you feel like c++ has become like the toxic parts of academics/math where everything is expressed in the most complex way possible to establish superiority over others.

The comments about debug ability are true. STL sources/template stacks are absolutely terrible to work with. Luckily with the STL though, what 99%+ of the time, the bug is in your usage, so it doesn’t matter much. To be fair though that is a trade off - I feel like I’m on vacation debugging c# or Python, but that is (almost) an intrinsic benefit of interpreted languages, with the well-known associated costs. Just templates... the cognitive load of having to understand (simply read) and debug code from an abstract definition introduces significant pain in terms of debug ability and general usability.

“C++ is not the STL” - true but c’mon - whatever is built-in mostly defines the stuff that you can be sure is portable, and rely on as a standard practice/resource in large projects. Very few sane people want 5 different implementations of a vector or string from god knows where with god knows what bugs. In the industry for c++, there is serious need for the _option_ to “reinvent the wheel” of standard libs, but it shouldn’t be that this is necessary to achieve baseline performance or usability in common use cases.

I am of the same opinion as at least one other below - simplicity of implementation and readability of language are king in the “real” world. What’s better than the beauty of a meta-programmed, absolute masterpiece of modern C++ and zero-cost abstractions? A program that I can understand in 10 minutes two years after it was written, or better yet that can be understood and debugged by 90% of skill levels instead of 10%.

This conversation could, and does, go on without end. My net-net conclusion so far is that modern C++ has done more harm to itself than good because it is trying to be too much. When the creator of the language can’t even keep up with it enough to call himself an expert, it’s a pretty obvious red flag that things have gone off the rails.

[+] smallstepforman|5 years ago|reply
Kind of agree, and the root cause may be backwards compatibility so another construct is invented which is slightly different.

The other day I explored std::promise and std::future, only to realise that all it is under the hood is a semaphore (which starts with count of zero) and a pointer. The promise.set() updates the pointed memory and releases the semaphore, while future.get() waits for the semaphore and reads the memory. My workaround was just as many lines of code with abstractions that are no more complex. So whats the point of promise/futures? Async adds spawning a thread to the mix, yet another unneeded abstraction. Coroutine adds more. Just be done with this and add proper Actor model and call it a day.

[+] Hitton|5 years ago|reply
I don't understand why does he show loop-based version using 2 for loops instead of using just one. I usually work in higher-level programming languages, but can't believe that cache hits or something else makes it faster than finding sum and maximum both while going through all images only once.

And having just one loop makes it simpler too.

[+] Epskampie|5 years ago|reply
The author mentions that you shouldn’t use std::, but offers offers no alternative other than “write your own version that is shorter and better”.