Modern C++ gamedev: thoughts and misconceptions
vittorioromeo.info
vittorioromeo.info
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.
y = (f(x1), f(x2), f(x3))
vs y = f(x1, x2, x3)
Not entirely sure anymore though. But it was something about causing side effects but throwing away return values with the ,-operator and involved function calls, I thinkI mean, they're only readable to people who have dabbled in variadic templates in their free time. That's how many people on your (future) team?
This line of reasoning is vacously true for any syntax and semantics though. Move semantics and rvalue reference are only readable to people that have taken the time to understand them -- they're undoubtably useful though.
It is best to use them only in performance sensitive places and containers.
C APIs come, of necessity, with tons of documentation about who is deleting what, when. Or, you know, maybe they don't and you have to learn the hard way. std::unique_ptr (implemented with move semantics) largely solves this problem.
And you can imagine notions of ownership more complex than "I'm deleting this at some point" (maybe "I'm versioning this object now, don't worry about it"). If you want to encode these transfers of ownership into your API, that's Move Semantics!
The point is that you shouldn't be using rvalue ref parameters, std::forward, etc. in most of your code. Even std::move should be fairly rare.
Passing std types across API boundaries is a code smell.
"I have created an object and will pass its unique ownership to you." -> std::unique_ptr
"This routine needs a function that takes two ints and returns a float (without putting all my code into headers)." -> std::function<float(int, int)>.
Can you elaborate in what circumstance you should not pass std::types across API boundaries?
std::function is useful in some situations, but "without putting all my code into headers" is not a good argument.
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.
It's also much easier to avoid allocations to begin with than C# - pointers can be used as interface types without boxing (and taking interior pointers to fields / array elements is allowed), and it's possible to allocate regular Go objects / arrays in unmanaged memory and feed it into any API.
Obviously it's not really designed for it and the ecosystem isn't there but using it hasn't been completely terrible so far.
C++ can be a real bitch and commit one to a mental institution or it can be very helpful if one does not try to play PhD.
I write servers and some other stuff in C++ and find it incredibly easy to use. I just do not do any esoteric things.
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.
I should also add that Hungarian notation is the prototypical example of dumb, verbose code, that wants to make individual lines easier to understand by dragging type information into every single variable name.
If used wrongly. Joel Spolsky wrote a whole post on it[0], but the TL;DR is that you should use the notation to differentiate between variables of the same type. For example, you might have world coordinates and object coordinates in a game script. Correctly used Hungarian notation would denote them, for example, with `wPosX` and `pPosX`. Even though they're both int (or float), you can easily see that you shouldn't assign one to the other.
Using them to notate types, however, as in `iPosX`, is completely useless. I fully agree with that.
[0] https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
That is not something you want in modern C++. Quite the opposite, in fact, and many projects avoid Boost for that reason.
Templates should be used when needed, no more.
- The official server (arcturus) is awful to work with code-wise. But all the decently big private servers uses it because at one point we only had the binaries of it and it worked out of the box. When the source started to leak too, it was easier to continue forward with that thing.
- The "simple version" which I am assuming you refer to is os(i)rose. This was the only thing you would get BEFORE the official server got leaked. It had some momentum simply for being there since roughly 2006. It was based on Brett19's code which at the time was a 14-something teenager. The same brett that now works on a fully modern C++ codebase that is decafemu.
- The Modern C++ version which if I remember is worked by few folks from osrose came out something like 2 years ago. Passing 2015, the momentum for the game is close to none. So yes, no one will even spin that codebase.
The official server was stolen due to horrible code-practice (C++-wise and software engineering in general), like having plain SQL-injections when creating characters. Worst! This was one of the reason that made the company (TriggerSoft) behind the game go bankrupt. The game was full of security holes back in 2005. This made the game's economy being broken due to few cheaters, created few horrible roll-backs and such. This drained the player's base from the game.
The "simple C++ server" osrose was also plagued by security issues and technical issues. Up to a point that people preferred to patch the official server with dll-injections + assembly rather than trying to make this "simple C++ server" work.
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.
That can also be perceived as a a flaw of the language design in that it does not allow one to write dumb, safe and fast code. Which are such languages in existence today?
edit: fixed typos
- Dumb: you bet. The inclusion of lambdas has shaken things a little, but usually Java code is straightforward with little space for "cleverness". The counterpoint to this, of course, would be the memes about AbstractFactoryFactoryImplementationSubclassDispatcher class names and all that, which IMHO does not represent much actual Java code out there. There are good reasons why big corps prefer Java, and readability is one of them. As a programmer, I've found it easier to jump into a Java codebase I didn't know much about than in any other language. And this has happened even when I had little experience in Java.
- Safe: yes. You have to go out of your way to be unsafe in Java. Memory allocation is done for you and the try-with-resources idiom is almost as good as C++ RAII.
- Fast: also yes. Usually about 2x or 3x the run time of C/C++ code, some times even less.
As a technology it really didn't have much to give. Object Pascal was born in 1986, Ada in 1980 with language support for design by contract, JIT with Lisp in 1960 and Java came in 1995.
- Too many jumps in code logic instead of serial logic
- Premature optimization 1: messy code (using lots of unclear variables etc), this is common within calculations, and games have quite some of those.
- Premature optimization 2: failing to properly architect
- Single-character variables. Java IDEs default to fullvars
- Bad function/method names
- Not expressing what you mean (for i= vs foreach)
- Too many abstraction layers / IoC
- Too many sideeffects / complex code (interwoven code)
- Callback hell
Nr 1 is important.. You need to be able to "follow the code". If that requires you to create a DSL in order to code something async in a serial way, then by all means do so.
For example with: network (duh.), but also in games: character dialogs, animations etc. etc.
C++ offers "efficient" ways to be clever, with varied and difficult challenges that can be addressed in relatively little difficult code; some are good or harmless (e.g. aligning struct fields to cache lines or concise towers of useful templates) and some are bad (e.g. flaky homemade not-too-smart pointers and almost-STL-compatible containers).
Java, on the contrary, facilitates the creation of large, boring and easy to read generic object-oriented tumors, that become satisfactorily clever only when they go very far (e.g. one more layer of indirection than everyone else) or reach theoretical limits (e.g. nothing left to invert control of).
The inclusion of lambdas has shaken things a little
I'm curious, do you consider lambdas as more or less "dumb"? Because I consider them the dumbest, simplest and maybe best way to do polymorphism. In a way, OOP's whole shtick was about not using them and instead extend stuff with classes.not commenting on the Java part as I believe it's usually faster than that (though not so sure when you see the years of hoops that Minecraft java had to go through to stop being so damn slow all the time...) , but it's kinda frustrating to be fighting for microseconds almost daily and then hear people saying that 2x slower is fast... 2x slower means going from 100fps to 50fps which ... well, gets you fired ?
Using classes for everything with a full OOP approach, instead of DOA and ECS, with tons of new in hot paths, no wonder it had performance isuses.
This was discussed in some Minecraft forums,
https://www.reddit.com/r/programming/comments/2jsrif/optifin...
It basically boils down to
> Why is 1.8 allocating so much memory? This is the best part - over 90% of the memory allocation is not needed at all. Most of the memory is probably allocated to make the life of the developers easier.
HFT is as high demanding as games and it makes use of Java, however I think that Java developers with such skills rather have a HFT salary than what game devs earn on average.
I remember a story of a HFT trading software written in Java. Supposedly it had big issues with GC. That's why they built a system where multiple threads would attempt the same operation, and the first thread wins. This approach reduces the likelyhood of a GC ruining the timings. Funny story.
My bachelor's thesis involved writing a software in Java that would manage dozens or hundreds of millions of small objects. These objects were all instances of the same class; the contained only three ints. It was very slow, and especially in an OOM situation the GC would work for more than a minute before finally giving up. I changed the software to use SOA instaed of AOS - moving from a huge array of these objects to three int[] arrays. Since ints aren't boxed, that left me with only 3 objects instead of many millions. The code was uglier for it, but the performance was another world. Unfortunately, such a change is not practical if you have many classes.
That was 5 years ago with Java 8. Disclaimer: I haven't followed Java since then. I know next to nothing about it.
There is an EA available for Valhalla and there is now the roadmap to incrementally bring such features into the platform. Java 14 has a new native memory support as experimental and it might reach stable already by 15.
https://jdk.java.net/valhalla/
https://cr.openjdk.java.net/~briangoetz/valhalla/sov/01-back...
https://openjdk.java.net/jeps/370
The problem why it is taking so long is engineering effort to keep ABI compatibility, namely how to keep 20 year old jars running in a post Valhala world, while at the same time migrate value like classes into real value types.
Java's biggest mistake, from my point of view, was to ignore the GC enabled systems languages that had value types, non traced references, and AOT compilation from the get go, then again I guess no one on the team imagined that 25 years later the language would be one of the choices in enterprise computing.
Back to Minecraft, the game isn't Crysis or Fortnight in hardware requirements, so a language like Java is quite alright for such game, what isn't alright is what the new development team apparently lacking experience eventually ended up doing to the game engine.
If one is to believe a couple of posts like the one I referred to.
With regards to performance, there must be some fine art in splitting structs into smaller structs, and keep them as parallel arrays, but there is also a limit to it. At some point you will need too many pointers to point at the same position in all these arrays.
I've never cared to split a lot, since it makes code harder to read. My guideline has always been to optimize for modularization: In OOP there tend to be large objects containing links to "all" related information. That violates the rule of separation of concerns. With parallel arrays you get perfect separation of concerns. One parallel array doesn't even need to know that there are others.
> Back to Minecraft, the game isn't Crysis or Fortnight in hardware requirements, so a language like Java is quite alright for such game, what isn't alright is what the new development team apparently lacking experience eventually ended up doing to the game engine.
I'm not in a position to judge, and I've never even played it, but it seems to me that Minecraft has a lot of voxels to maintain. Also massive multiplayer requirements?
As on the client side, proper coding plus offloading stuff into shaders already goes quite far.
And even in the debatable point that Java isn't the best language for any kind of game development, well maybe Mojang would never have taken off if Markus had decided to prototype the same in something else.
Nowadays the Java version is only kept around due to the modding community, as the C++ version is found lacking in this area.
Naturally at some level you will have a class full of native methods as FFI to OS APIs or libraries written in C or C++.
From that point of view, I consider Java still a better option as game engine scripting language as Python/Lua/JavaScript, because you get strong typing, there is still dynamic loading, a good set of AOT/JIT/GC infrastructure and more control over memory layout than those languages allow for.
Naturally that is a matter of personal taste.
Java as a language is dumb, but as an overall platform is not. In a way it is a two-layer platform: you have the outer layer, a boring language that is used by a lot of people to write most of the code, then you have a hidden layer, that most people ignore, made of bytecode manipulation, runtime code generation and language agents that let you do cool things like adding compile-time nullness checks, generate mapping classes to avoid writing a lot of boilerplate code or good old ORMs that automatically generate your sql queries for you.
Such functionalities are not exactly easy and straightforward to use, but in my opinion it is a good thing: they are there and can be used, but for most programmers will be hidden behind a few "magic" annotations.
This is in contrast to other languages where the advanced functionalities are "all over the place" and every programmer must be aware of them (I'm thinking of C++ and Common Lisp for example).
If you have a teams of great programmers you may achieve better results with the latter approach, but for the average company the Java approach is better because you can have average programmers write boring code while taking advantage of a few clever tricks here and there by using libraries/frameworks written by better programmers.
Zig will be there soon.
> The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
~ Rob Pike
I don't know that this is, ipso facto, evidence of anything about C programmers other than that they really hate C++, though.
The experience of writing new code in one is very, very similar to the experience of writing new code in the other, right down to the compile times. The lifetime analysis in Rust is nice and pretty far ahead of what static analyzers can do in C++, but Rust Generics are a pretty weak approximation to Templates. Rust has better Browser integration, C++ has Qt. One imagines the languages will catch up to one another on these fronts. C++ has Inheritance, Rust settles for Interface Polymorphism (one can reasonably prefer either).
The one really big difference here is actually cultural - the Rust community all agrees on Cargo, and it's a bit happier to compile the world and distribute static binaries, which removes massive headaches for both the developer and the end user while setting the language up for an eventual, Electron-style tragedy of the commons where a user is running like 8 Rust apps with their own copies of the same 14 libraries resident in memory (but that's a good problem to have because it means you've displaced C/C++ as the linguae francae of native app development).
I guess the other really big difference is that there is no legacy Rust code.
I like C++, but I can understand hating it. But if you have written new code in C++17, and hated it ... I suspect you are going to hate writing Rust too. And if you love Rust and hate C++ ... I suspect what you hate is legacy C++ from 2005.
Finally, I was explicitly not concluding anything about C Programmers beyond that they hate C++.
Rust's generics allow for some seriously powerful abstractions to be built in a very clean and readable way, although there can be friction with stuff that would be simple with templates in C++ and quite verbose in Rust.
Maybe concepts will change that.
Template (at their origins) are nothing more than generics, and a pretty clean, powerful and zero cost way of doing generics.
What you name "hazardous, unergonomic" macros style is not the template system itself. It is mainly due to all the 2005-styles SFINAE hacks that have been invented by abusing templates properties.
SFINAE in C++ is nothing natural, it's at best a dangerous trick to have compile time resolution/execution.
Fortunately all of that should die progressively with C++-17 constexpr for the good of humankind.
C++ also makes it way too easy to reach for shared_ptr instead of unique_ptr leading to all sorts of unfortunate things. Rust makes that much harder and RefCell/Rc/Arc push towards design that are "single owner" which I've found scale out much better once you move into programs that are of a significant complexity.
C++ still wins in portability on some platforms but I have a hard time picking it for anything greenfield at this point.
But, yeah, there are a ton of domains (notably embedded) where I would want Rust.
However with the support of companies like Microsoft, Rust will eventually get there.
By the way BUILD 2020 will have Rust sessions.
When is the last time you accidentally mutated a raw pointer? My opinion is that references are just another C++ feature that solves a non-existent problem and has severe disadvantages. And that is non-orthogonality / combinatoric type system explosion. I've seen more than one codebase that consisted to a large degree of adapters for calling the same functionality.
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.
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.
The problem is that every problem is caused by a multitude of causes, and there’s usually no clear way to distinguish issues caused by lack of discipline from issues caused by the wrong coding approach or style.
Different styles make different demands on how disciplined a programmer must be in order to write correct code. My experience in well-managed large code base with decent-size teams is that the recommended approaches and coding styles will evolve to address problems that could be solved by asking programmers to be more disciplined.
So that’s the problem with saying “no coding approach will ever solve an issue caused by lack of discipline”—it only makes sense if you already have a good idea of which issues are caused by a lack of discipline, and if you already understand that, then the saying doesn’t help you. The key insight here is to understand when changing your coding approach may allow you to write correct code with less effort (and discipline). But this can’t be distilled into an aphorism, so it won’t get quoted.
An example is holding a lock to access a certain field. If you’re “disciplined” you can just leave a comment on the field that lock X must be held to access it. But in practice, you want to solve that with code analysis. Another issue is using scoped locks so early returns don’t screw you. You can easily argue that these issues aren’t caused by lack of discipline—but if you travel back in time 20 years or so, “discipline” might be the only tool you have to solve them.
Ideally, a program's specification and implementation would be one and the same. The more the program departs from a plain-language specification, the more room for error exists, and the more discipline is required to avoid those errors.
IMO, what we need are better specification languages and better tools to compile them, not better programming languages. I believe we've gone as far as we can go with the latter. Some might even say that watershed was crossed in the COBOL era.
There is no reason simple code can't have higher-level constructs. The interface to them is just likely very domain specific, and you don't start with them.
But after you notice you are copying the same sort of code to a third place, you usually notice a pattern, extract that pattern (with no abstract frills attached) to a unique implementation that can be used everywhere, and move along.
"Higher-level constructs might require language features like template metaprogramming, or a level of indirection."
I think we have different definition what a "higher level" means. To me it means a particular pattern has been identified in the code and lifted to an implementation that needs less thinking and fewern lines of code.
You can have quite high level clever program logic using the basic algorithmic toolbox - the basic containers and large zoo of well known algorithms to operate on them - the array, the list, the map and the graph.
To give you a sense of how I think about this, we can just focus on control flow. One option for control flow is very uniform and easy to grasp for anyone, including a programmer from 1950: there are [conditional] gotos and labels. Another option is if/else, while, for, try/catch, yield, return f(),...
So what gives? Particular patterns of gotos/labels were identified and lifted into a situation that needs fewer lines of code. Does it need less thinking? That's where it gets tricky. It's obvious to me that the programmer in the 1950s will look at try/catch and require much more thinking than if they just had goto/label code in front of them.
Template metaprogramming (I don't understand what usage is distinct from STL-like usage) is exactly about identifying a particular pattern and literally lifting a concrete type into a type parameter so that now you have a related family of code. Analyzing the code requires higher level and lifted thinking, the same way that manipulating algebraic expressions instead of concrete numbers does.
That's what I mean by higher level.
I agree that code should strive to reuse "fundamental" container types (implementation, or at least interface), but I don't see the connection to the current conversation, aside from the feeling that using those containers without lifting the types (whether in your mind, or in the language) is impossible.
I believe GP was referring to the myriad techniques for using templates not as containers, but as type-level functions, and composing compile-time programs that rely on SFINAE, variadic templates, andffunction overloading rules to express functional programs in the C++ template language. You can find examples in boost in various areas.
One somewhat spectacular example is boost::spirit/boost::qi, which allow you to define parsers with a DSL directly in C++ (e.g. using `*c` as '`c` 0 or more times').
You can pass a flag to baz, to choose either foo or bar. Now you have a closed set of possibilities. If you want to extend the functionality e.g with a plug-in, or got any other reason you want to avoid committing to the choice, then you either need
1. first class functions, so that you can pass in foo or bar or whatever.
or if you don't have first-class functions, then you need a
2. FooLikeFactory to make a FooLike object based on a runtime value (e.g. read from a configuration file), and then you can call your FooLike object from baz.
I like the quote that design patterns are bug reports against a language. The factory stuff you're talking about doesn't just exist for fun. It solves an actual problem. I hate Java as much as you do, I'm sure, but I value understanding the reason the patterns exist before I decide to just use a language where I don't need to do any of that stuff.
a common pattern I see that leads to this:
1) construct a relatively expensive object. 2) use that object to do A, B, and C (each of which require setting up their own smaller objects)
there are a lot of different places where people want to do A, B, or C, but not necessarily all of them. but people are reluctant to break A, B, C out into their own helper functions because of the cost to construct the object and possibly the very large number of parameters that need to be passed. with enough time/effort, it is possible to detangle this stuff and encapsulate it more sanely, but it's usually easier to just follow the existing pattern.
I want to work where you work.
I've seen literally 4 separate implementations of the same UI component in the last week. It's obvious that they all started out from a common base (e.g. by looking at variable names, function names, etc). However, over time, each component has diverged, as each project that the component was copy-pasted into just made changes willy-nilly to their copy of the component, rather than recognizing that they have a copy of a shared component and refactoring changes upstream or building common abstractions upstream.
Now I've been tasked with doing the refactoring work to make these components DRY, and I can already tell that it's going to be far more work than management has anticipated.
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.
var person = new Person();
var car = selectCarById(carId); // Car
if type is not obvious, it should be explicitly declared.`doSomethingToACar(selectCarById(carId));`
Kind of weakens the argument. I'm not sure what's the best approach, but I'm usually ok with autos even when the type is not explicitly known - when reading code, I do not really need to know what exact type a variable has ("it's a car, goddamnit, it says so in the name!"), just how it's used (and then meaningful function names become very important).
for(auto iter = vec.begin(); iter!=vec.end(); iter++)
for(std::vector<project_namespace::class_name>::iterator iter = vec.begin(); iter!=vec.end(); iter++) for (auto i : vec)Especially when that "auto i : vec" should have instead been "auto& i : vec" or "const auto& i : vec" and now you are at best wasting cycles and at worst writing to copies that will soon be discarded, ending up with a bug that can be very hard to spot.
for(Class entry : container)
This is still an uncontroversial improvement over having to typedef or use auto for the iterator.Generation of a single solution: 3 easy lines (calling on a few hundred lines of goofy math that actually describes the structure, but that's common to all of these approaches)
Writing a for-loop to fill a std::vector of solutions -- about 10 lines of a familiar stack-walking pattern which could confuse a novice.
Making a fake container that defines a begin() and end() along with a nested iterator class: about 20 lines of necessary boilerplate, another 20 lines to replicate the stack-walking, now sprinkled about the boilerplate. The novice is completely bewildered, so we add another 10-20 lines of comments to explain it.
So I have this strong urge to keep the first two implementations in place, just to provide a gentler ramp. But I won't use the code in the end, so it would only add maintenance overhead, so a lone tear rolls down my cheek as I delete the clear, readable code.
In python, this is often as easy as changing square brackets to parentheses to change a list comprehension into a generator.
The debate is about all the other cases.
I know i do not use any of the languages you mention, for example - and if i did, i'd explicitly write any type names.
But honestly i can only talk about me here, i can't guess why some imaginary other developer who dislikes a feature does dislike it.
How do you reconcile that world view with the fact that people are shipping billions of line of codes that obviously work in languages where until recently you couldn't even write any type anywhere (JS, Python) ?
For others, it was worrisome because it made code easier to compile, and rightly so. If it complies it doesn't mean it's correct.
I mean, sure, if i take that code and run it through a debugger - perhaps while also crossreferencing the features it uses at cppreference.com - then i'd be able to follow it. However at that point any relevance to readability would have been thrown out of the window long ago.
For me, this whole spread/fold feature is easy to grok, because I've worked with many similar features elsewhere. In this case, the feature looks almost identical to how `...` is handled in one of the Scheme macro systems, syntax-case (and syntax-rules, by extension)[1]. As mentioned, the use of spread on a comma operator with a side-effect is tricky and maybe too clever, but, otherwise, I don't see anything out of ordinary.
> perhaps while also crossreferencing the features it uses at cppreference.com
That's the thing - if you already knew the meaning and syntax of these features by heart, you'd find the code using them very readable. You also wouldn't need to step through it in the debugger, because there's really not much happening there in terms of control flow.
In general, "readability" is simply a bad word to use: it's too overloaded and means too many things to too many (kinds of) people. Every language can become readable to you if you put enough effort into it; and no, the amount of effort needed is also dependent more on your prior knowledge than on the language in question. So it's just too subjective to be useful as a metric for anything, unfortunately.
[1] https://docs.racket-lang.org/reference/stx-patterns.html#%28...
Yeah, but the effect of this is greatly overstated most of the time. As I said, given enough effort, you can learn - and learn to keep in your head - anything. It matters in the short term, while you're learning, but in the long run, once you've learned and internalized all the required information, it stops being relevant.
It's probably harder to learn to read and write kanji instead of the Latin alphabet. For most people, that difference matters for a few years in their childhood, but once they have the characters drilled into them, it no longer matters: they can read and write as well as any Westerner.
The same is true for (natural) languages: some are inherently more complex and hard to learn than others, yet once you become fluent, you stop noticing the complexity. You simply speak, read, and write your thoughts directly, without thinking about grammar and spelling too much.
It's also visible in sciences and engineering. Mathematical notation is especially notorious: not only every symbol can have multiple meanings, but you're expected to also guess which meaning was intended from other symbols and text around. That's on top of introducing hundreds of made-up words for equally made-up concepts, like a "number", or "monoid in the category of endofunctors".
Finally, it manifests in programming and programming languages. In various ways. For example, there are some people who use APL, K, or J - because it's "easier to read and keep in your head a single line of APL than a 500 loc of equivalent C". If given a chance, they will tell you that something like this is of course very readable and straightforward:
⍝ John Conway's "Game of Life".
life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}
You just need to learn a few things first, and that may be hard, but once you do - I'm told - reading and writing code this way becomes effortless, and a thousand times more efficient than writing in C.Basically, if you're going to be switching languages every year, then yes, there's a difference between having to learn the language for a month rather than six before you can ship something. On the other hand, if you're going to stick with a language for a decade or two, then the long learning process becomes irrelevant, as it's dwarfed by the rest of the time where you actually use the language.
> it still is hard to follow the flow
It may be hard if you're not familiar with the common patterns of using higher-order functions. HOF and lambdas are not GOTO: there's a structure there, it's just richer than the basic set of if/for/while statements. You could call such a structure an FP equivalent of OOP design patterns.
> calls to other functions call back to local lambdas
Yeah, but that's also true for every abstraction, starting with a procedure definition. Also, you don't need lambdas to have this problem, it's enough to register procedure as a signal handler, or register an event handler in some async framework. When you pass a comparator function to `qsort`, you similarly don't know when and how that function will be called, even though it's a named procedure.
To summarize: no matter the language, you can learn it, you can fit all of it in your head, and you can make it readable for you. It requires effort, which is an investment: it might not be worth your while, depending on your circumstances. However, if you encounter a code you don't understand or have trouble with reading, because you didn't invest enough time into learning the language, that's not the code's (or features') fault. Just be honest with yourself and don't blame others for what is a result of your conscious decision.
Also of note: yes, the features often do differ in their complexity, and the differences influence the readability (for lack of a better word). However, to see this and to be able to compare, you have to first learn the features in-depth.
Edit: also the default comma operator discards its lhs, it is pretty much always used for its side effects.
[1] https://github.com/milessabin/shapeless/wiki/Feature-overvie...
I already wrote that there are some cases where auto is necessary (usually when used with more recent C++ features).
> especially if an IDE is there to expand complex type information
And i also already wrote that this information is not only often cumbersome to obtain but also such an IDE is often not available - e.g. in a web-based code review tool which also happens to be an environment where you want the code to be most understandable.
This usually means types that are from inside the scope of another class. Compound types that are used frequently can actually be aliased.
Also writing programs that are clear when reading from plain text is great, but I don't think that should ever be a higher priority then what it is like to work with inside an IDE. The days of writing programs with notepad are over thankfully. Languages aren't the only way to make programming easier and aren't even where the low hanging fruit is. People get caught up in languages, but tools can help much more without the herculean effort of redoing decades of work, so I lean on them whenever possible.
However, about IDEs, you still ignore that code is not only worked with inside IDEs - i already wrote twice the case of a code review tool... have you ever worked on a team with code reviews done by a web-based tool? Or even with a source control program that you want to check the differences between commits that someone else made long ago (they may not even be at the company anymore) and the diff tool obviously has no idea about types and such?
There are many reasons for why you need to work with code outside of an IDE and none of them have to do with using Notepad to write the code.
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.
Fix a bug somewhere ... pray to the Nasal Demons you got there before more than two developers duplicated it elsewhere.
You just need to know functional programming.
Know your tools, people.
It's always "ugh, this code is a copypasta convoluted mess. I'm going to replace it with a simple solution".
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.
A lot of times it can also mean "an abstraction that my development environment is unaware of or otherwise has deficient tooling for"
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.
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().
It reminds me of the arrogance displayed in the Twitter thread, which is a shame. I think like someone said, experience is valued only by those who have it.
I've been there too though, I was that arrogant young programmer once thinking all these old-timers weren't a match for my technical skills. It usually passes.
Well, that would be a good question to ask to the author, since he titled the article "modern C++ gamedev" and not simply "modern C++" although to me what is discussed seems to be general enough, and "gamedev" here happens to be just the type of project the code comes from... To me, it does not look like he makes any significant contribution on how "modern C++ could serve specifically games development". IMHO he has detected that people in games react strongly to content about C++ and he benefits from the additional exposure he gets by putting "gamedev" in his C++ articles/tweets/anything (which is something I am not judging btw).
Personally, I do think that when your code is simulating a whole parallel universe using a not-super-high-level language, you might end up with challenges regarding readability and flexibility of the raw code that other softwares might not encounter. Not being really part of that industry I feel I can't know for sure, and therefore I have applied the same reasoning to him since he is also not part of that industry, it seems.
I don't think questioning credentials is particularly elucidating here. I don't see any reason the entire thesis of the article is flawed.
As to the throwaway reference to gamedev, the article is in response to shade thrown by gamedevs, attempting to concede specific concerns and propose solutions.
I do find the recommendations interesting and would like to keep the conversation about them. I would especially like to avoid gatekeeping and No True Scotsman arguments in a thread about code.
So now you're straw manning me as well, then say I'm gatekeeping and no true-scotsmanning. I don't appreciate that all and am therefore out.
- At least someone on Twitter called some tweets "retarded", etc., so OP thought to continue the conversation by ignoring nonsense and restating concerns in a healthier tone. This post is his attempt to be productive, apparently.
- Any perceived slight against you, you have inferred. No offense intended.
- Perhaps HN discussions are only marginally better than Twitter if people can't avoid making discussions about fold operators personally.
Well, not really. Some of the people on twitter (and the author himself) basically pointed out that "one of the main reason we do not use that coding style is that, despite the advantages, when you need to work on a codebase that requires both performant simulation of the planet and rapid experimentation of mechanics at the same time by hundreds of de-localized devs, you end up seeing many of its limitations". If you want to handle this argument, you need to have a clear idea of what are the priorities in such a scenario, which I doubt the author has. Even more interestingly, the article does not raise the point "this is how I write modern C++, I think it will benefit all the industries" but specifically seems to want to argue that this style helps gamedev particularly despite showing basically no domain-specific observations, nor anything that convinces me he knows what are the critical problems in the "triple A game" scenario... I don't think I can call this gatekeeping if I am not convinced - even by reading only the article - that the author has a good grasp of the fundamental problems involved in the development of big games, or not?
While making big assumptions about the author's experience in the process. It's plausible he has experience in complex, performance-sensitive, highly collaborative C++ as well. The whole argument hinges on partial information and disbelief, really.
For me the code on the right is far more readable and easier to understand.
- 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.Local consts are not particularly useful, especially for things like ints. Compilers know it's const, and a reader doesn't need to worry about it in a local scope.
width, height usually come in pairs, so putting them on the same line is usual.
nPixels and nBytes are not used more than once so they are not useful abstractions, and the pattern of `width * height * bytes_per_pixel` is so common that there is no ambiguity about what it does.
The idx lambda is probably a distracting abstraction which requires the reader to think of it in a different context than the immediate what pixel goes to where. Again, the pixel moving pattern in the right is so common it's familiar to most people working in similar areas, and in terms of error-prone both are not better than the other.
Same thing with doing two separate things in the loop. Not only can it stop the the compiler from optimisations from time to time (the c++ compiler is very clever... but many times it gives up), I've had speed increases in breaking it up into several loops, but I also have to search and in my mind break things up. Very easy to do when I'm writing the code (and I'd probably do the same because of laziness), but quite annoying when I read someone else's code that does it.
The argument about optimization is almost certainly premature optimization. Most of the time how you write a loop doesn't matter. You only find out what matters via profiling and refactor accordingly.
If the compiler can enfore an invariant (constness), why cede that functionality on the hopes you can ensure it yourself?
By the same line of argumentation you should do away with the static type system.
In that case, it makes sense.
The changed version is idiomatic in image processing or similar areas that deal with pixels. Being idiomatic makes it familiar to read, and easy to change.
It's under that lens that I am criticizing the changed code.
I can still assign to it. Sure, you can argue that if the code is complex enough that you might accidentally do that without it being obvious, then the code should be simplified or split up, but that's besides the point. If its const, the compiler will complain, if its not, it will silently let me. Nobody can write ideally-factored code all of the time and following good consistent practices helps. Also, I hear arguments like that all the time, but so much of actual real-world shipped code breaks these "rules", so I'd rather be pragmatic and choose a style that improves otherwise imperfect code.
Its also as much a hint to future me that the variable is intended to not be modified.
> 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.
[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.
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.
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.
I get the sense the author's deliberately stirring controversy.
Separately, I think programming itself is solitary and predisposed to people who don't focus on building social skills (myself included). I think online (pseudonymous) communication can introduce all sorts of problems.
[1] https://www.youtube.com/playlist?list=PLTEcWGdSiQenl4YRPvSqW...
> This function attempts to allocate a temporary buffer equal in size to the sequence to be sorted. If the allocation fails, the less efficient algorithm is chosen.
Or is this not what you consider a "functional construct"?
But the C++ versions of these take the result location as an argument. It makes chaining them together a hassle because you have to create all the intermediate containers explicitly.
I think there are good reasons for the STL to work this way, but it can make programming in a functional style pretty inconvenient compared to a lot of other languages.
See e.g. here: https://github.com/ericniebler/range-v3/tree/master/example
Then he wrote a blog post on it.
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.
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.
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.
And having just one loop makes it simpler too.