Abnormal programming

Myths, superstitions and folk wisdom in game development

Nov 24, 202523 min
Whoever comes to us bearing wisdom will be the one to fix it

Whoever comes to us bearing wisdom will be the one to fix it.

There's quite a lot of common "wisdom" about C++ game development, various rituals and kinds of magic. And as is often the case with such sacred knowledge, on closer inspection some of it really does have a right to exist, some can be shipped off to the Cairo Museum to steal the mummies' thunder, and some turns out to come from a different reality entirely and flatly refuses to work the way it was supposed to. But that doesn't stop some companies from treating such advice like tablets carefully carried down from the great mountain of meetings. New hires receive them almost with the solemnity of an initiation rite: "This is how our ancestors did it, so this is how we do it."

At other shops — usually the young and brash ones — they may wield instruments of various lengths for laying things on, or hoard like Plyushkin and drag into the project everything that lies around for free or unguarded, smearing an already complicated architecture with a thick layer of abstractions, tools and frameworks until the whole thing simply stops working. In that kind of atmosphere it's already hard to tell whether the wisdom works, or whether it just got drowned under a dozen layers of engineering creativity. So it's better to treat any "truths" calmly and test them in practice rather than by their age. By all means, write in the comments whether each item is a myth or folk wisdom — I'd be curious to hear your opinion. This turned into a longread, so brew some tea, or something stronger, there won't be any pictures... almost, and it also turned out that horses, people and flies got blended into one big meatball, with musings about development and C++ interleaved with studio superstitions — whoever's worked at a studio will understand.

(M) "Use the F(orce), Luke"

The battle cry of a whole school of developers — and from fairly well-known game shops, too — who are devoutly convinced that every line of C++ automatically causes an FPS drop and awakens an ancient evil in the compiler, and who propose living in a strict world of pure C where everything is predictable and the compiler doesn't try to look smarter than the programmer, even though in a great many places it actually is smarter. The adherents of C usually arrive in gamedev from all sorts of areas of development, and it's very rarely possible to talk them into stepping away from the sacred dogmas.

Rank-and-file members of the cult of the Force usually cite the words of their leaders who never looked game development in the eye, and there are plenty of big names there: Rob Pike (creator of Go) has for many years consistently criticized C++ for its excessive complexity and the constant accumulation of layers of abstraction, saying the language grows too chaotically, offering dozens of mechanisms for the sake of backward compatibility, which makes it hard to fully understand and use well and breaks its architectural backbone.

Andrew Tanenbaum (author of the classic OS textbooks) has repeatedly criticized C++, noting that the language has become excessively complex and keeps bloating year after year, accreting ever more exotic constructs. That C++ has too much hidden magic, subtleties, exceptions to the rules, hacks, tricks and unpredictable behavior, because of which the language has lost the simplicity and minimalism it was once valued for in C, turning into a heavyweight tool that's hard to fully understand and control.

Among grizzled game developers you'll find no shortage of followers of Saint John Carmack. Though he treats C++ with respect, he has more than once stressed that the language's "magic" — in complex abstractions, operator overloading and templates — can easily lead a project away from real performance, especially if the programmers don't control the project as a whole and end up with unmanageable complexity, putting the emphasis on simplicity and predictable code behavior. So his criticism is ultimately tied more to the practical use of the language in games than to the language itself, which is why you so often hear "Well, Carmack wrote it that way and bequeathed it to us."

If you stand next to such people during their flare-ups, you involuntarily start to look at all this horror of C++ development from their point of view, and Purity begins to beckon with Terrible Force. But the moment you step away to the kitchen for a cup of coffee, you notice that all these statements were born in a systems, low-level or research context, and transplanting them directly into gamedev is about as correct as treating a game engine with advice from people who write firmware for hardware — that's also about code, and the advice is also right, except for a couple of BUTs...

Whether it's wisdom or a myth is ultimately for you to judge, but OS code changes much, much more rarely than game code, so keeping the game engine working and maintainable takes priority — and it doesn't matter what language it's written in. You can be an adherent of the Force, but forcing others to rewrite everything in C is clearly too much, although an overdose of modern C++ isn't good either — but that's a separate story I'll come back to.

(S) The bad good build

This belief lives stubbornly in gamedev, finding confirmation here and there, which only makes it stronger as it migrates along with other superstitions from one studio to another. On its own it sounds absurd to newcomers: "A build that passed through the hands of overly meticulous QA and came out nearly perfect fails external testing more often than a build with a certain set of bugs." You might think this is some separate Murphy's law for developers, but no... it's just a quirk of human psychology. When the QA department reports "all clear, ready to ship," the team relaxes and stops catching bugs; builds often go out a bit late and people manage to cram a fair number of bugs into them, while managers report "it's almost final, look only for critical problems." And then the vendor discovers that saves break, and a specific combination of graphics settings leads to a stable crash, or that on certain video cards the game was definitely written by people on substances.

If external testers see the UI is a bit crooked, or a menu animation stutters, or the character occasionally T-poses, their attention is no longer as sharply focused on trying to break the game in non-trivial ways, climbing into corners of maps "where a normal developer doesn't go," or checking what happens if you save during a cutscene. This state of heightened QA paranoia even has its own name, "smell test alert," something like "something smells wrong here." A very strong QA department that catches everything in the early stages creates a paradoxical problem for "conditionally normal" development, when you need to push the build forward toward release despite bugs and minor flaws, turning development into an endless wheel of "find-and-fix" iterations and eating up developer time on tiny and super-tiny tasks. A new feature gets stuck in QA for a week while testers beat every possible bug out of it; in theory this may even be right — quality should come first — but in practice it kills the creative impulse and smothers bold decisions under an avalanche of reports and week-long fixes.

Whether it's a myth or a superstition is for you to decide, but producers who've survived a release know this problem and deliberately keep QA on a short leash in the early stages of development, handing builds to juniors and giving the team freedom to break things and experiment with mechanics, while the especially ferocious testers are let loose only in the final stages before release.

(FW) Exceptions are free, almost...

There's a fairly popular opinion, pushed more than once at various game talks and conferences, that exceptions in C++ are an evil of evils and cost an outrageous amount, and that the people who use them should be thrown into a cauldron. At the same conference, the very next talk rolls out a presentation showing that modern table-based exceptions "eat" almost nothing and the cauldron turns out to be no bigger than a kitchen ladle — until the exceptions decide to actually pop out.

I'm fairly skeptical of both camps, and in the current engine exceptions are on in debug and off in release — there is a difference, it's not quite zero, but close to zero — let's say you can see a roughly 2-3% drop between builds, but for a debug build that drags in third-party NVIDIA libraries, also debug, which (oh horror) are also built with exceptions, that's acceptable. But with today's processor power — the work machine churns away with 24 cores — those 2 percent, well, who cares about them, if the FPS is steadily 60. Usually what really hits performance is some completely different code that you only start thinking about when you see it in the profiler. Maybe in the old days there really was overhead from the exception mechanism, but now it's been buried under core counts and gigahertz. But, as usual, in real life these little things are almost unnoticeable… until someone decides to put exception handling into some hotstack function that gets fired far too many times per frame, and that's when the white arctic fox arrives and it turns out they're not so free after all. We ran into the bug on one family of compilers with a specific set of options.

Whether it's wisdom or a myth is for you to decide; as with any overdose of modern C++, mindless use of exceptions doesn't make the code faster or simpler either, and the effect is almost imperceptible — until the fox arrives and reminds you that "almost free" doesn't mean "absolutely, definitely and completely free always."

(M) A separate cauldron in hell for lovers of smart pointers

Smart pointers are often considered a categorical "don't use" in games: first, they were considered so; second, that was about fifteen years ago, but the memory of it is alive to this day. The problem here isn't the smart pointers themselves but the memory allocations associated with them, and those really can hurt performance — and if you replace some std::unique_ptr<> with a plain new, nothing changes except the extra burden of having to handle that very new correctly in the destructor. An allocation stays an allocation; now you just also have the risk of a memory leak, a double free and the other joys of manual memory management, and there's no escaping that. It's allocations that kill performance, it's they that destroy spatial data locality, it's because of them that your cache turns into a pumpkin — or rather into Swiss cheese. But... I had an acquaintance, an adherent of orthodox development in the style of the apostle Kernighan, who would actually write the horror of horrors malloc(sizeof(IMPL_TYPE)) and then call the constructor by hand, but he was a tech lead with respect and a lot of experience, so no one on the team dared say a word against him. If you're not an elderly tech lead with a license to malloc — don't do that.

std::shared_ptr<> is another matter — there's something to think about there, because the atomic reference count, the separate control block in memory (inline or external), the extra indirection on every access, integrity checks and so on and so forth, all of that has its price. If you use objects only within a single thread and don't share them across other systems, you can almost always find a lighter alternative: intrusive pointers, non-atomic counters, or just a clear understanding and management of the object's lifetime without all the bells and whistles. And here an array of objects that simply aren't unloaded from memory will be a perfectly good solution — we're not piloting a spaceship, after all. But within game development, given the use of modern game engines, not using smart pointers is practically impossible; to begin with, a texture, a model, sounds and a behavior tree — i.e. the basic objects a game object is made of — will 99% be structures built on smart pointers. Good if these are variants optimized for use in the engine; worse if it's plain STL.

Myth or folk wisdom — your choice. The claim "smart pointers can't be used in games" once had some basis, perhaps, but many years have passed since then — compilers have gotten smarter and engines have accreted abstractions. Today not using smart pointers in development is practically impossible, and there's no need to fear them — what you should fear is mindless allocations.

(S) Fear STL containers

With STL the situation is more complicated; I love the standard library, except maybe for string handling — there are better and faster options there — because it's portable, perfectly documented and predictable in its general behavior on one platform and one compiler. But when it comes to high-performance code, like games, there are too many questions to keep using it.

And the main question is about memory. STL loves memory very much, and that's not a compliment — for example, std::map and std::set allocate memory on every (every) insert. std::string during concatenation can reallocate several times in a single operation; I've written about strings more than once, and STL is something of an outsider here. Every call to new/delete is not just a call to an allocator, usually the system one, because nobody bothers to set the project up even minimally with allocators or pools. Fine, never mind memory, there's plenty of it now even on consoles, but an ordinary allocation is a blow to data locality — all our objects end up scattered across all available memory, and when traversing a list of more than 100 elements the processor will spend more cycles fetching data from memory than on the actual work with the objects, and now you're staring at the profiler and can't figure out where your milliseconds went.

There are solutions, only they're remembered less and less often: custom allocators, memory pools, alternative implementations like EASTL or folly from He-Who-Must-Not-Be-Named-In-The-Media. But each of these solutions is a compromise between development convenience — because you have to drag it in, update and maintain it, teach new people to work with the custom library — and control over performance. There's no silver bullet, and you'll have to choose between readable code on standard containers and tight control over the performance of the code you write.

The folk wisdom actually goes like this — <use STL containers in moderation, don't use STL just for the sake of using containers, use arrays where it's possible or applicable>, but why is it that out of the whole set, all that stays in people's heads is the banal "don't use STL"? Here it's important to understand what you're getting into when you write #include <vector>, <map> or <list> — every container is built differently, and that difference isn't just an abstraction dreamed up by Stepanov, but very concrete milliseconds in the profiler. std::vector<> is your first and best friend, consisting of a single contiguous block of memory where all the data lies side by side. Yes, when it grows beyond its capacity there will be a reallocation, but that happens rarely and it's that rare case where STL does exactly what's needed. std::list<> is a doubly linked list, and each element will be a separate block somewhere in memory, and they'll lie however they please but definitely not next to each other — and if you wanted to use a list in your code and can't explain why, it's probably not needed there. Same story with std::map<> and std::set<> — every insert will be done via an allocation. Traversing the tree is jumping around memory; from a locality standpoint it's roughly the male approach of hunting for socks all over the apartment instead of looking for them in the dresser drawer.

Myth or folk wisdom — for you to decide. But if your hand reaches for map<> or list<>, maybe it's worth stopping and thinking again. Not because these containers are bad — they do exactly what they were made for — but because in nine cases out of ten the task is solved by a linear array. Solved better, faster, more predictably and more simply. Complex data structures aren't a sign of good architecture but a tool for specific tasks. As Chandler Carruth, the vector evangelist from Google, used to say — if you can't explain in thirty seconds why you need a tree or a list right here, you probably need a vector.

(FW) Don't seat people with the same name next to each other

Among the unwritten rules of game studios there's a rather strange one that experienced HR managers and team leads try to observe — don't seat two people with the same first or last name next to each other. Doesn't matter whether it's Alexander, Dmitry or Max, but if a second bearer of the same name appears on the team, they try to place them a seat away in the open space, in another room, sometimes on another team. A grown, sensible person will of course just laugh at such behavior, but statistics are a stubborn thing, and in half the cases one of the namesakes left the studio within six months. And usually it's the more experienced one who leaves — the one with more leverage on the market, the one who can afford to choose. Our in-house psyHiatRist, in heart-to-heart kitchen talks, blamed it not on magic or curses but on the banal psychology of identity and territory, which looks like coincidence.

The problem isn't people with the same name, but that if they sit next to each other, everyone else quickly develops communication crutches: "Sasha-senior," "Max-the-new-one," "Dima-the-programmer" versus "Dima-the-designer" — using last names in address and nicknames in Slack partly solves it, but those crutches only work on the surface. It creates an invisible tension, a mirror effect where each involuntarily compares himself to his namesake: who's faster, who's more useful, who gets praised more, whose ideas get accepted. Sooner or later one of them gets tired of this unconscious competition and finds a place where his name won't be compared, so it's easier to seat a new person two seats away, on another team, preventively, than to spend six months looking for a replacement for a departed developer who "just decided it was time to move on," when in fact he simply got tired of being one of two Sashas.

I'd probably have laughed along with you at superstitious HR if at one point I hadn't been in a similar situation myself. And even though I was two months into lazy negotiations about moving to another studio, one unfortunate Monday they sat my full namesake across from me. It was funny and amusing, and that evening we threw a little party at a nearby bar in honor of the new-old guy — but statistics are a funny thing sometimes, and a month later I moved to a different position. Whether it's a superstition or a myth is for you to decide, but within that half-year I've already twice heard similar stories from acquaintances separated by several thousand kilometers.

(M) This new <insert your own> will solve all problems

Sooner or later a person bitten by Unreal, Boost, folly or Rust (no flame war, sorry to those who use Rust for work) appears at the studio with a proposal to rewrite everything onto the racially correct library, engine, approach or language. Such people are usually quickly buried under an avalanche of tasks, and sometimes the person can be saved — or else they go looking for another place to apply their talents. The question of using <insert your own> comes up everywhere sooner or later, but so far it's "your own" that's actually been the success in development. My position on this took shape long ago, in my first years after I got into gamedev, and hasn't changed much since.

First, almost everything that people used to drag boost, folly, absl or something else into the project for is now in the standard library — shared_ptr, unique_ptr, optional, variant, any, filesystem — all of it long since moved into std, or was adapted in a custom container implementation. In fact half the features of modern C++ existed in some form first in Boost, then were tormented by comments in proposals, and then became standard. <insert your own> is great on presentations to the bosses, but its problem is that it starts dragging half its guts along for the sake of a single header, and once it has jumped into the project, you'll later be tearing your hair out trying to pry it back out. boost, folly, absl are monsters with hundreds of libraries, and half of them drag the other half along behind them; at the phrase "let's add a little Boost" from people of varying degrees of cursiveness I'm soon going to develop a nervous tic. And they do add it, so that six months later you discover twenty packages from <insert your own> in your dependencies that nobody on the team knows how got there.

Second, most likely you're just missing certain containers like flat_map, static_vector or small_vector — these really are useful things, and STL won't ship them any time soon. But here an important point appears: almost all of this is also in EASTL, in a variant originally sharpened for aggressive gamedev and with the option of getting by with a couple or three headers. In the worst case all of it can be written, ahem, "communized" over a few evenings, and if there's no other option, dragged into the project as an external dependency. But choosing among all the options, I still lean toward using EASTL or individual headers from it, as a solution tried and tested on many projects. And even if there's a real need for something from a third-party library, especially a big one, it's better to weigh the consequences for the build, binary size and performance, and only then make a decision. Most likely it'll turn out the task is solved by three lines of plain C++ or your own solution.

Myth or folk wisdom — your choice; the phrase "we need boost/folly/absl for this task" sounds solid and grown-up only at meetings with bosses who don't know the first thing about what Boost is or why folly is in the project, because all that absl will be integrated, fixed and used by completely different people. In my experience (opinion may differ from the employer's) behind such loud phrases there's usually either an unwillingness to dig into the task, or nostalgia for a previous project where it was already plugged in and working — plugged in not by the author of the initiative, and working without his fixes or involvement. And if a year from now you'll be explaining to an intern why the build takes twenty minutes and where in the project boost::fooly::thread came from — don't say you weren't warned, it won't solve anything.

(S) Polymorphism is a great evil

Another sacred mantra of gamedev: "polymorphism is evil." It sounds convincing, especially if you say it with a serious face in front of the mirror in the morning, or at a code review while rubbing juniors' noses in AI-written code. But let's figure out what exactly we're calling evil.

Polymorphism has two kinds of cost: the first (explicit) — to call a virtual method, you need to go into the object for the pointer to the vtbl, then follow that pointer to find the function's address, and only then call it. The problem has been known for a long time: the processor can't (well, almost can't) predict where execution will go, and it resembles a branch misprediction. It sounds scary enough, until you remember that the vtbl almost always sits in the cache if you access objects of that type even somewhat regularly. And this isn't the place where milliseconds are lost, because processors have gotten faster, caches bigger and branch predictors smarter, so they already remember double jumps just fine, which is exactly our case with the vtbl.

The second kind of cost (implicit) — and this is where the pain begins, because polymorphic objects are traditionally created via new. And new is an allocation, which I write about probably in every other article. Objects live somewhere on the heap, scattered across memory, the cache suffers -> the game lags -> players are unhappy. And this part of the cost of using polymorphism is real, but it's not about calling virtual functions — although you can squeeze a couple of FPS there too — it's again about how the engine and the developer operate on objects.

How to fix large arrays of objects has also long been known: object pools, placement in arrays, custom allocators, reuse and sleeping copies come to the rescue here — all of which will work beautifully with polymorphic classes. In my whole career I've seen maybe a couple of times when polymorphism itself, in the form of a virtual function call, became the cause of noticeable performance problems — it was when we shoved a virtual call into matrix multiplication. But that was a unique case that was caught and fixed very quickly, and in all the other cases it was problems with allocations, data locality and how data is placed in memory. And at conferences talks about destroying virtual calls go over better than talks about working with memory correctly, which everyone knows about, but apparently the instruments tip the scales.

Whether it's a myth or folk wisdom is for you to decide. It's convenient to blame polymorphism for all performance drops; the very phrase "virtual function" already sounds suspicious, and the vtbl looks like a problem to be overcome. Polymorphism is a tool, and like any tool it can be used crookedly. A virtual call isn't the enemy; the enemy is ten thousand objects scattered across the heap that the processor reaches through three pointers to a fourth.

(FW) RTTI is a small evil

Also known as Run-Time Type Information, it's a mechanism that lets you find out the type of an object at runtime. Sounds useful, costs a lot — roughly 7% of performance. Not that it's horribly, terribly expensive, but enough to make you think: do you really need it in your game? Well, unless, of course, you have a spare five FPS lying around... but usually the question is the other way round, where to scrape together those five FPS for new features. That's why in most game engines RTTI is disabled at the compiler-flag level.

In my whole career I haven't seen any other use of RTTI in real games besides the use of dynamic_cast<> itself. Maybe somewhere in a parallel universe where code is written exclusively by the books of Alexandrescu and McConnell there are such game engines, but in ordinary gamedev RTTI is a synonym for dynamic_cast<>. Yes... sometimes it's used in tools and editors, but not in the game runtime.

Now about dynamic_cast<> itself — in the vast majority of cases it can be replaced with a virtual function. Instead of asking "you wouldn't happen to be an Enemy?" you should just add a virtual cast method toEnemy/asEnemy to the base class that does what you need. Namely — and this is a big, fiercely guarded secret — it will return this, while the base class will return null. And it's faster, clearer and more reliable, and requires no RTTI at all. In game code this approach also maps better onto the architecture: you have a base Entity, it has virtual methods asDamagable(), asEnemy(), asFaction() — and no casting is needed, everything is resolved through overriding.

So if dynamic_cast<> suddenly ended up in hot code, well, someone probably just didn't do their job, and dynamic_cast<> is a fairly large overhead, with a full traversal of the type hierarchy with string checks and a bunch of other stuff. The only case when dynamic_cast<> can be justified is when you physically can't change the base class and it's in someone else's library, it's generated or carved in stone and guarded by a dragon. Then you can use dynamic_cast<> and plaster the spot with comments about why. In all other cases — if the base class is yours and you control the hierarchy — there's no justification for dragging RTTI into the runtime. Myth or folk wisdom — your choice. RTTI is a convenient tool — you write dynamic_cast<> and let the compiler figure out what type this Vasya is. If you control the base class, control the casts too, and leave the sugary solutions for tools, editors and those who have spare frames per second to burn.

(M) You mustn't call a build final

There's an iron belief that you mustn't call a build "final" until it's been officially shipped to the publisher. The moment someone utters "this is the final version" or names a folder final in any variation, within the next twenty-four hours a critical bug is guaranteed to surface that forces yet another iteration. It's like pilots and submariners and their use of the word "last" in the sense of "final," so developers know this and use all sorts of euphemisms like "release candidate," "submission version," "almost done," but never "final." Especially old and seasoned developers outright forbid the word "final" in folder and branch names in the repository in the last weeks before release, because this isn't a superstition but a fact confirmed by the statistics of bug trackers over the past umpteen years. So the correct build numbering looks like this: RC1, RC2, RC3, RC_almost, RC_really, and only after the official shipment to the publisher can you exhale and call it final_final_for_real_this_time_v2 with a date in the name for insurance — we know about version control, but it's like with backups, better to make a backup of the backups that have been verified to be restorable.

Myth or folk wisdom — your choice. You can consider it just the ravings of superstitious product managers and lazy QA folks (more on them above) and coincidences, that bugs surface randomly and have nothing to do with folder names. But when you see this repeat from project to project, from studio to studio, you start to understand it's not about magic. It's probably that the word "final" changes the team's attitude toward the code and the game on a subconscious level, and that affects code quality.

(S) Let's write it in assembly

Sometimes, in a fit of doing the project good, the temptation arises to rewrite a particular spot in assembly. Theoretically asm can still speed up a program; practically, it's never worth the problems it drags along. The cases when it's genuinely needed are so rare you can count them as natural anomalies, and the problem here isn't that we've forgotten how to write assembly — the problem now is the complexity of the processor and the environment: the correctness of the code you write depends directly on the context in which it's used. Forget one of the dozens of conditions for writing such spots and the fun begins. In one place the code works, in another it doesn't, in a third — a dragon flies in and eats your binary. But of course in debug everything works, since it has its own little world there where the rules are correct. Moreover, under MSVC it (asm) is completely absent for x64, so you can forget about portability and you'll have to drag small binary files around with the project that are linked outside the sources. So by choosing asm you automatically sign up for the adventure of "guess the code from the stack trace."

Once upon a time programmers had a good reason to dive into assembly — the SSE instructions; today that era is over, because intrinsics have worked just as well for ten years now. They give access to the same instructions but without the pain, are compatible across compilers in most cases, and are far less prone to summoning the dragon. In the end asm remains a tool of last resort, not of optimization, but specifically when nothing else helps anymore. It's worth applying in application code only when you've confirmed through profiling that this very spot is the bottleneck, the very narrowest bottleneck, and no intrinsics give the needed result, and that you're prepared to put up with dragons flying in and eating your binary. If even one of these conditions isn't met, better to let the compiler do its job. No assembly will give you 5% of performance — good if there's a 1% gain and no bugs. Myth or folk wisdom — your choice. Inline asm isn't an optimization, it's a statement — a statement that you're smarter than the compiler, that you're prepared to maintain this code on all platforms, and that you don't mind the time of colleagues who'll be reading it a year from now. Sometimes such a statement is justified — about one case in a thousand. In the other nine hundred and ninety-nine, intrinsics do the same thing, only without dragons, without undefined behavior and without nighttime calls from QA. Let the compiler do its harmful work — that's what it gets electricity for.

(FW) Template bloat

One of the ancient fears of early C++ was "template bloat" — the situation where the compiler, when instantiating templates, creates mountains of nearly identical code and the binary swells to indecent sizes. It sounds, you know, scary, especially if you read about it in old books. In practice I haven't seen it become a real problem — unless, of course, you write something like a recursive template that unrolls into twenty levels of nesting and generates code the size of a small operating system. Though no, I lied to you: on one of the mobile engines there were problems with templates — there were simply a lot of them, really a lot, and MSVC's internal 4 GB paging file wasn't enough to hold such a large number in memory; it was solved by a compiler option that forcibly set the size of that file larger. And clang chewed through it without any problems at all — its only problem was the from-scratch engine build time, which was on the order of half an hour.

Moreover, today templates are used even on microcontrollers, where memory for code is kilobytes, not gigabytes. If they manage there, then in a game engine on a console or PC templates are definitely not the bottleneck. Fortunately we're not flashing toasters, so we can afford std::vector<T> without pangs of conscience. The only thing really worth avoiding is deeply recursive templates and metaprogramming for the sake of metaprogramming. When a template instantiates a template that instantiates yet another template, and all of it unrolls into some compile-time something that the compiler chews on for twenty minutes — that's when the problems start. And not just with code size, but with build time, and readability, and your colleagues' desire to talk to you.

If you're still worried about code growth from templates, there are techniques to combat it — moving common logic into a non-template base class, explicit instantiation and other tricks. But in my whole career I've never once run into a situation where it was acutely necessary. Usually the problem is solved more simply: at review you explain to a colleague how not to write insane code, until that commit is ready for the engine.

Myth or folk wisdom — your choice. "Template bloat" is a scarecrow from the nineties that's still used to frighten juniors in interviews. Yes, theoretically templates can bloat the binary, and theoretically the compiler may run out of memory. But in practice this happens about as often as meeting a unicorn — possible, but unlikely to be you specifically. If your code builds, works, and doesn't make colleagues cry at code review, then templates are definitely not your problem. And if MSVC did fall over with out-of-memory, well then, congratulations, you've written something truly outstanding. Now all that's left is to figure out why.

(M) Premature optimization

I'm talking about those changes that don't get in the way of development, don't worsen code readability and don't break portability. In some sources, for example Sutter, refusing such easy optimizations is even called premature pessimization. One of the simplest examples of such an optimization is making sure that the increment of iterators in loops is written as ++it and not it++. Any postfix operation for non-integral types formally requires creating a temporary copy. The compiler can eliminate that copy, but why make it do extra work if you can just write it correctly from the start? All the more so since it costs practically nothing.

Another point — passing structs and classes by const reference instead of by value. If you still don't do this, you're probably doing something wrong. Although since C++11, thanks to the copy elision mechanism, which isn't required by the standard but is implemented by almost all modern compilers, it's sometimes more advantageous to pass arguments by value if you're going to create a copy inside the function anyway.

There's also a myth that returning values through non-const reference parameters, i.e. using output parameters, is gradually going out of fashion. Or using initialization instead of assignment in constructors, when people write str(s) instead of str = s; — especially useful for strings and other types with dynamic memory allocation. In addition, it's worth defining move constructors and move assignment operators where possible and marking them noexcept where applicable.

All of this falls under the kind of optimizations that don't create problems during development and cost literally "nothing," while applying them lets you avoid premature pessimization and makes the code a bit more efficient without extra risk and headache.

Myth or folk wisdom — your choice. Premature optimization is evil — everyone's known that since their university days — but premature pessimization is an evil no less splendid, it's just talked about less often. None of the things described worsen readability, break portability or require all-nighters with the profiler. It's simply the habit of writing code that doesn't do stupid things without reason. And stupid things without reason are what you later pay for in milliseconds, nerves and the question "why are we lagging out of nowhere."

(S) The curse of the Friday commit

Every game developer — and maybe non-game too — is familiar with the fear of the Friday commit. It doesn't matter that it's correct, verified on a working version, passed the tests and solves a "little problem" that can be "quickly fixed." Usually the Friday commit breaks the build, so the next six hours go to rolling back and fixing. The superstition says that the closer to Saturday and the smaller the change, the higher the chance of breaking everything. "It's just a couple of lines" — how often I've heard these words before heading off for the weekend, at least a couple of times a year for sure. The rational explanation: fatigue, haste, lack of time to think the solution through properly — and even experienced programmers with long tenure fall for these "just a couple of lines." Good thing there are wise team leads who forbid committing to master from 9 to 12 on Friday, because they know about the Friday urge to "quickly fix a little thing" and that this urge is stronger than reason and tests. And if someone does push through with "but it's a critical bug," well then, flag in hand, drum around the neck, and Saturday as a workday, which sharply raises conscientiousness and overall responsibility, often pushing the fix to Monday.

Myth or folk wisdom — your choice. The Friday commit is a convenient opportunity to land a bug and go home with a feeling of duty done. Save the heroic impulses to "quickly fix it before the weekend" for startups, crunch evangelists and those who have nowhere to put their Saturday. Statistics are a cruel thing, and nine out of ten "quick fixes" on Friday evening smoothly flow into debugging on Saturday morning, while the remaining one just hasn't broken yet and is waiting for Monday. So if you really want to commit something on Friday — make a branch, push it, and let it sit until Monday. Over the weekend either a problem in the code will turn up, or it'll turn out it wasn't needed at all, or on Monday you'll merge it with a clear conscience. Committing to master on a Friday evening is like playing Russian roulette with five rounds — technically you can survive.

To be continued?

That's probably it — at least for today. There's no shortage of myths and folk wisdom in any field, and if this one landed, let me know and we'll keep dismantling the sacred tablets of gamedev. Meanwhile, a question for you: what other "truths" about game development have you heard that, on closer inspection, turned out to be either outdated, or torn out of context, or simply true? Write in the comments — I'm curious to gather a collection and break it down next time. And yes, if you're that very person who writes malloc(sizeof(Class)) and then calls the constructor by hand — write too, I'd like to understand how you came to such a life.

← All articles