Abnormal programming

C++101 (Part 4)

Jun 21, 202638 min

C++101 — a four-part catalog of C++ idioms and techniques: Part 1 · Part 2 · Part 3 · Part 4

Contents of this part

Making New Friends (tricky)

From the previous section comes the realization that correct declarations of friend functions for template classes can ensure that every instantiation of a template class has its own friend function, correctly bound to it. When you declare a friend function inside a template, you get either "one function template that befriends all instantiations" or "a separate one just for a single instantiation."

The canonical approach is to define the friend function right inside the body of the template class (this is the very "hidden friend" trick, a relative of the Barton-Nackman trick); then for each instantiation of the template a separate non-template friend function is generated, visible only through ADL and working correctly with exactly that instantiation. This "makes a new friend" for every concrete type, hence the name, but you can also declare a friend template, or a friend of a specific instantiation, which requires forward declarations and delicate fiddling to make the types match.

What we pay here is the combinatorics of the interaction between templates, friendship, ADL, and name-lookup rules, and it is very easy to get it "wrong": you can end up with a function that isn't found, or is found but is the wrong one, or conflicts across multiple instantiations. The modern consensus is to always prefer hidden friends (definition in the class body), because they are both simpler and more efficient at overload resolution (they don't pollute the global candidate set).

The idiom was described long ago and is discussed in detail in Vandevoorde's works, and its relevance has grown in recent years thanks to the "revival" of hidden friends as recommended practice in work on compilation-time optimization and error-message quality, where it has been shown that hidden friends noticeably unload the compiler compared to operators declared at namespace scope.

template <class T>
class Vector3 {
    T x_, y_, z_;

public:
    // hidden friend and a separate non-template function for each Vector3<T>,
    // found only through ADL, correctly bound to this instantiation
    friend Vector3 operator+(const Vector3& a, const Vector3& b) {
        return {a.x_ + b.x_, a.y_ + b.y_, a.z_ + b.z_};
    }

    friend T dot(const Vector3& a, const Vector3& b) {
        return a.x_*b.x_ + a.y_*b.y_ + a.z_*b.z_;
    }
};

Vector3<float> a, b;
auto c = a + b;     // finds "its own" friend through ADL

This is used when writing templated math, of which games have plenty in the form of templated vectors, matrices, and quaternions parameterized by a scalar type (float, double, half, fixed-point). They all need operators (+, -, *, dot, cross), and defining these operators as hidden friends inside the template is the cleanest and most efficient way, giving the operator the correct binding to each instantiation without polluting the global namespace.

The practical benefit of hidden friends in a large templated codebase is not so much correctness as the effect on compilation speed and error-message quality: now operators hidden inside classes don't participate in overload resolution for unrelated types, and the compiler has to consider fewer candidates.

In an engine with extensive templated math this adds up to a tangible difference in build time, so making new friends is no longer only about "how to make it compile" but also about "how to make it compile fast and with understandable errors."

Non-member Non-friend Function (tricky)

From the two previous sections a design principle was born, which states that if a function can be implemented as a free function that is neither a member of a class nor its friend (that is, using only the public interface), then that is exactly how it should be done. It sounds counterintuitive, because we are used to stuffing everything into class methods.

But encapsulation is measured by the number of functions that can break when a class's private data changes, and every method and every friend has access to the private members, which means it potentially depends on them and may suffer when they change. A free function that is not a friend has no access to the private data and can only work through the public interface, so changing the class's internals cannot affect it.

Consequently, the more functionality is moved out into such free functions, the less code is tied to the private details, and the higher the encapsulation. Paradoxically, moving functions out of the class actually makes the class more encapsulated, not less.

The price for this is a move away from "object-oriented" programming toward global functions, and it requires a conscious effort, which often sparks arguments within a team. On top of that, free functions need to live somewhere (usually in the same namespace as the class, so that ADL finds them), and there end up being many of them. And not everything can be moved out, but the idea itself, that a class provides a minimal complete set of primitive operations as methods, and everything else (convenient combinations, algorithms) as free functions on top of them, is still alive among game developers today.

The principle was formulated and fiercely defended by Scott Meyers in Effective C++, and was later backed by Herb Sutter; under their influence it found its way into the standard library, where the algorithms are the most vivid examples of free functions, and even many string operations are pulled outside. After C++11 this became one of the directions of the modern "interface-minimalist" class design in development.

class Vec3 {
    float x_, y_, z_;
public:
    float x() const { return x_; }       // minimal public interface
    float y() const { return y_; }
    float z() const { return z_; }
    void set(float x, float y, float z) { x_=x; y_=y; z_=z; }
};

// length, normalize, lerp are free, via the PUBLIC interface.
// Changing the private layout cannot break these functions.
float length(const Vec3& v) {
  return std::sqrt(v.x()*v.x() + v.y()*v.y() + v.z()*v.z());
}

Vec3 lerp(const Vec3& a, const Vec3& b, float t) {
    return {a.x()+(b.x()-a.x())*t, /* ... */};
}

From the standard library this principle "leaked" into all the modern math and geometry types of engines, which have a huge set of operations. Making length, normalize, lerp, reflect, project, angle_between members of Vec3 would mean bloating the class and tying all these operations to its internals, whereas moving them into free functions keeps Vec3 compact, with a minimal set of basic operations, while a rich library of operations on it is built on the outside without touching the private data.

This also gives practical flexibility: new operations on the type can be added without editing the type itself (important when the type lives in someone else's library), and they can be grouped by purpose rather than dumped into one bloated class. That is why compact math types plus extensive namespaces of free functions over them have become a practical way to keep the key engine types small, resistant to change, and extensible.

Small Object Optimization (SOO/SSO)

This is a technique in which small objects are stored right inside the wrapper, in an embedded buffer, instead of allocating memory for them on the heap; if the object is small enough to fit into the space reserved inside the wrapper, no allocation happens at all. And if it is larger, the wrapper falls back to the heap.

The most famous special case is Alexandrescu's Small String Optimization (SSO), where short strings (usually up to 15-22 characters) live right inside the std::string object, without going to the allocator.

The motivation was to get rid of heap allocations, which are expensive and unpredictable, while strings are more often short than long. The second candidate is std::function, which more often wraps a small lambda than a huge functor. It is usually implemented through a union of an embedded buffer and a pointer plus a flag/discriminator "am I small or large right now," and all the logic is hidden behind the interface.

The price you pay is the complexity of the implementation and the larger size of the class, which is deliberately bloated to hold more data: a larger buffer means rarer allocations but worse density in arrays. The "small/large" logic with unions, placement new, and manual dispatch of copy/move is highly nontrivial and easily contains bugs. Plus, moving an object with active SOO means physically copying the buffer (you can't just steal a pointer), which is sometimes more expensive than for the heap variant.

This is long-standing practical knowledge about implementing strings and containers, and SSO has been used in implementations of std::string for decades (libc++, libstdc++, MSVC STL, each with its own threshold and layout), while std::function, std::any, and many type-erasure wrappers use SOO to avoid allocating for small callable objects.

// Schematically: a small value lives in the buffer, a large one on the heap
template <class T, std::size_t N = 32>
class SmallBuffer {
    alignas(T) std::byte inline_[N];
    T* ptr_;

    bool on_heap_;
public:
    template <class... Args>
    SmallBuffer(Args&&... a) {
        if constexpr (sizeof(T) <= N) {
            // fits, put it inside
            ptr_ = new (inline_) T(std::forward<Args>(a)...);
            on_heap_ = false;
        } else {
            // doesn't fit, allocate on the heap
            ptr_ = new T(std::forward<Args>(a)...);
            on_heap_ = true;
        }
    }
    // ... destructor, move, copy taking on_heap_ into account ...
};

In games SOO is traditionally considered one of the most valuable optimizations, because engines are obsessed with avoiding per-frame allocations, and small objects are everywhere. The SSO idea led to custom engine string types, and to containers like inline_vector/fixed_vector (storing the first N elements inline and going to the heap only on overflow), and even to maps and lists, where it is used for short lists (a few collisions, a couple of child nodes, or a handful of tags) that would otherwise spawn allocations, while EASTL and similar libraries provide such containers out of the box.

A well-chosen buffer threshold turns "an allocation for every sneeze" into "an allocation only for the rare large cases," and in a memory profile this shows up as a sharp drop in the number of allocator calls. That is why understanding SOO and the ability to use inline containers is a common practical skill in optimization, as one of the most direct ways to remove allocations from the hot path.

Prohibiting Heap-based Objects

In games it is common to distinguish where certain objects are allowed or forbidden to live: only on the heap, only on the stack, or anywhere. Sometimes the design requires that objects of a certain class be created exclusively through new (for example, because they are managed by a reference counter that calls delete this, which means an object on the stack would lead to a crash on the attempt to delete itself). And sometimes the opposite: an object must live only on the stack (for example, a scope guard, where the whole point is being bound to a scope, and creating it on the heap would be a usage error).

You can forbid heap creation by making operator new private (or deleted) — then new MyClass won't compile, and the object can only be created as a local/member variable. The reverse is forbidding stack creation, which is done with a private destructor: if the destructor is inaccessible from the outside, the compiler can't create an automatic object (it has nowhere to call the destructor on scope exit), but creation through new works, and deletion goes through a special public method destroy(), which calls delete this from inside, where the destructor is accessible. This "forcibly" drives the object onto the heap.

The price for both restrictions is fragility and usage discipline, which violate the user's expectations ("why can't I create this on the stack?!"), and a private destructor on top of that breaks inheritance and value membership. The idiom almost always signals that a class has an unusual lifetime model, and it is worth accompanying it with a fat comment.

All of this was discussed in detail by Scott Meyers in More Effective C++ (a separate item on "how to limit the number of objects" and related techniques about placement on the heap/stack), and it is a classic method for taking control of the object-creation model, tightly tied to reference-counting (Counted Body) and to special lifetime requirements.

// Heap only: a private destructor prevents creation on the stack
class RefCountedResource {
    ~RefCountedResource() = default;
    // private => can't be on the stack
    int refs_ = 1;
public:
    void release() { if (--refs_ == 0) delete this; }
    // deletion from inside, where the dtor is accessible
};
// RefCountedResource r;        // error: destructor inaccessible
// auto* p = new RefCountedResource;  // OK

// Stack only: a deleted operator new prevents heap creation
class ScopedLock {
public:
    static void* operator new(std::size_t) = delete;
    // can't go through new
};

In game development the "heap only" case naturally arises for objects with an intrusive reference counter (resources, RHI objects), which manage their own lifetime through delete this and therefore must not live on the stack. And "stack only" is needed for RAII wrappers like scope guards, profiler timers, and locks, whose semantics are rigidly bound to a scope, and creating them on the heap would be a logical error that cancels the whole point of RAII.

But in modern engine code these restrictions are more often expressed more softly: instead of a private destructor they make a factory returning an intrusive_ptr/shared_ptr (which already directs to the heap and at the same time provides safe ownership), and instead of forbidding new they simply put [[nodiscard]] on the scope guard's constructor and catch it in review.

The hard versions of the idiom are reserved for cases where the compiler really must catch the error, because the cost of incorrect placement is most likely a runtime crash. They are worth knowing in order to understand unusual classes with private destructors and deleted new, and not to try to use them "normally" and run into mysterious compilation errors that hide a deliberate restriction of the lifetime model.

Storage Class Tracker

A rare and rather complex idiom that lets an object find out at runtime where it is placed: on the stack, on the heap, or in static memory. It is related to the previous idiom, but it solves not "forbid" but "find out and react," when the object wants to behave differently depending on its storage class, or at least make sure it was created correctly.

The implementation is tied to comparing addresses to determine the approximate boundaries of the stack (for example, by the address of a local variable in the current frame) and the heap and to compare the address of this against them. One technique is to override operator new, which sets a flag "the next object is being created on the heap" right before the allocation, and the constructor then checks this flag; if it wasn't set, then the object was born not through new, i.e. on the stack or statically, and resetting and setting the flag around the allocation lets the constructor distinguish a heap object from the rest.

The price you pay is undefined behavior and portability, because comparing addresses with the stack/heap boundaries relies on assumptions about the memory layout that the standard doesn't guarantee, and the flag trick in operator new breaks with multiple inheritance, arrays, and multithreading. This is a very fragile technique that works "usually, on this platform, under these conditions," which is why it is avoided in production, and more often the correct answer is to design the class so that it is known by placement (factories, ownership policies).

It is used more as a curious technique than as working code, and it illustrates how far you can go in trying to find out about your own placement, and at the same time why you usually shouldn't, because there is no reliable and portable way to do it in standard C++.

// Fragile technique: a "being created on the heap" flag set by operator new
class TrackedObject {
    static thread_local bool heap_flag_;
    bool on_heap_;

public:
    static void* operator new(std::size_t n) {
      heap_flag_ = true;
      return ::operator new(n);
    }

    TrackedObject() : on_heap_(heap_flag_) {
      heap_flag_ = false;
    }  // the constructor reads it
    bool is_on_heap() const { return on_heap_; }
};
// Unreliable with arrays, exceptions, multiple inheritance

In games the storage class tracker is not used in production because of its fragility, when you need to work stably across many platforms and compilers, where objects are extremely sensitive to platform differences in memory layout. Any technique that relies on comparing addresses with "the stack" and "the heap" may behave unpredictably on a console, due to active stack-protection methods.

But in debugging and diagnostic tools, like memory trackers and leak detectors that need to classify allocations, you can give it a try. Even there, though, they prefer more reliable mechanisms like custom allocators, which know exactly what they allocated and where, because they did it themselves, instead of guessing after the fact from addresses.

So the storage class tracker is worth knowing mostly as an illustration of the boundary between "theoretically possible" and "shouldn't be done," as the mere fact that an object can try to find out its storage class by comparing addresses.

Execute-Around Pointer

This is the idea of using a "smart pointer" that runs some code before and after every access to the wrapped object, transparently to the caller. You write ptr->method() as usual, but between your call and the real method some wrapping logic is inserted: something happens before every method call and something happens after it. This is the application of the "execute around" idea to individual method calls.

Now when you write wrapper->foo(), the compiler first calls the wrapper's operator->, which returns not the object itself but yet another temporary proxy object, and this proxy also has an operator-> that returns the real object. The temporary proxy is constructed before the call to foo (its constructor is the "before") and destroyed after the full expression completes (its destructor is the "after"). Thus the constructor/destructor of the temporary proxy wrap every method call with "before" and "after" code, and this works for any method of the object without enumerating them all.

What you pay is non-obviousness (the double operator-> and the temporary proxies, which are hard to grasp at first glance) and the overhead on every method call, when a proxy object is created and destroyed. Plus the "before/after" is the same for all methods and you can't handle different methods differently without complicating the scheme. The lifetime semantics of the proxy (it lives until the end of the full expression) introduces subtleties if the calls are chained. This is a treacherous technique, appropriate in narrow scenarios.

The mechanism was described by Kevlin Henney in the article "Function-Object and Execute-Around Pointer" as a special case of the broader execute-around theme (to which both RAII and scope guard belong), applied specifically to intercepting method calls through a proxy and a double operator->. This is a classic example of how operator overloading in C++ lets you embed behavior into syntax familiar to the user.

template <class T>
class LockingPtr {
    T* obj_;
    std::mutex& mtx_;
    struct Proxy {
        T* obj_; std::mutex& mtx_;
        Proxy(T* o, std::mutex& m) : obj_(o), mtx_(m) {
          mtx_.lock();
        } // "before" the call
        ~Proxy() {
          mtx_.unlock();
        } // "after" the call

        T* operator->() { return obj_; }
    };
public:
    LockingPtr(T* o, std::mutex& m) : obj_(o), mtx_(m) {}
    Proxy operator->() { return Proxy(obj_, mtx_); }
    // the temporary proxy wraps the call
};

LockingPtr<Inventory> inv(&inventory, inv_mutex);
inv->add_item(sword);   // automatically lock -> add_item -> unlock

In games the execute-around pointer finds use where you want to transparently attach some wrapping to every access to an object, like automatic locking/unlocking when accessing a shared object, or logging, or profiling every method call on an object being debugged, checking invariants before and after a modification, dirty flags (marking the object as changed after any mutating access).

But in the hot path the overhead of a proxy object on every call is usually unacceptable, so all this lives in debugging and infrastructure code, where convenience and transparency matter more than performance.

Temporary Proxy

This is a mechanism in which operator[] (or another access operator) returns not the element itself and not a reference to it, but a temporary intermediary object that can distinguish reads from writes and react to them differently. It is needed when detecting the fact of a write to an element through an ordinary reference is impossible, because the reference doesn't know whether it is being read from or written to. The proxy, however, intercepting operator= (write) and operator T() (read), can tell these situations apart.

The classic application is the implementation of std::vector<bool>, where the elements are packed bit by bit and you can't return "a reference to a bit" (a bit isn't addressable), so operator[] returns a proxy that on read fetches the bit and on assignment sets it. Another example is copy-on-write strings, where the proxy on reading a character doesn't trigger detaching a copy, but on writing it does. The proxy here is an "interceptor" inserted between the access syntax and the real data.

What you pay is "proxy leakage": auto x = container[i] will store the proxy rather than the value (the classic vector<bool> trap, which is why it is considered a "broken container"). The proxy is not a real reference, and code expecting a T& doesn't work with it, because you can't take the address of an element through a proxy. These mismatches make proxy containers treacherous, and vector<bool> is the prime example of why a "smart operator[]" can be more of a curse than a good solution.

All of this was discussed by Meyers (including in the context of operator[] distinguishing reads from writes, in More Effective C++), and std::vector<bool> with its proxy reference was standardized back in C++98, and it serves both as the canonical example of how people want to do it and as the canonical cautionary tale of "how it turned out" at the same time.

// The proxy distinguishes reading and writing a bit in a packed bitset
class BitArray {
    std::vector<std::uint64_t> words_;
    struct BitProxy {
        std::uint64_t& word; int bit;

        operator bool() const {
          return (word >> bit) & 1;
        }   // read

        BitProxy& operator=(bool v) {
            // write
            if (v) word |=  (1ull << bit);
            else   word &= ~(1ull << bit);
            return *this;
        }
    };

public:
    BitProxy operator[](std::size_t i) { return {words_[i/64], int(i%64)}; }
};

In games proxy access shows up in packed and bit data structures, of which there are many, like flag bitsets and masks, packed color or normal formats, where the element physically isn't addressable as an ordinary object and you can't get at it without a proxy. There the proxy is a necessity, and the only way to provide a convenient [] is to build your own syntax on top of non-addressable data.

But proxy containers must be treated with caution, because there is the auto trap, the incompatibility with code expecting real references, and the overhead of proxy objects, all of which make them a tool for special cases rather than for general-purpose containers.

Many engines deliberately avoid std::vector<bool> precisely because of its proxy nature, providing explicit bitsets with clear set/test methods instead of a deceptively transparent []. So it is useful to know about the temporary proxy, to know when it is justified, and to recognize why some []s behave unlike ordinary ones, and not to fall into the traps associated with that.

Address Of

A well-known mechanism for getting the real address of an object even when its class has an overloaded operator&. Yes, in C++ you can overload the unary address-of operator, and some classes do (usually for the sake of clever proxies, COM-like smart pointers, or DSLs), but the trouble is that after such an overload the expression &obj returns not the address of the object but whatever the overloaded operator decided to return, and generic code that needs the real address (for example, to construct an object via placement new or to store a pointer) ends up deceived.

You have to get around the overloaded operator& and obtain the address "the back way," so the object is cast to a reference to a character type (through a series of reinterpret_cast or const_cast), for which operator& is guaranteed not to be overloaded, the address of this character reference is taken, and the result is cast back to the desired pointer type. The chain of casts looks frightening, but the idea is simple: you just descend to the "raw bytes" level, where taking the address definitely gives the real address, and climb back up.

What you pay for is the dubious ability to overload operator&, which many consider a language design mistake (overloading address-of almost always does more harm than good, breaking generic code and intuition). That is, it is treating the symptom of a bad practice, and the implementation is fragile and verbose, and you shouldn't write it by hand.

The mechanism was born in the discussions around Boost, and its canonical implementation boost::addressof, which later entered the standard as std::addressof (C++11). It is exactly what the standard containers and allocators use internally when they need the real address of an element, in order to work correctly even with user types that have overloaded operator&. C++17 added a constexpr version, and the need for the idiom has been entirely shifted onto the library.

// A class with an overloaded operator& (e.g., a proxy) deceives a naive &obj:
struct Sneaky {
    int* operator&() { return nullptr; }   // &obj returns nullptr, not the address!
};

Sneaky s;
Sneaky* wrong = &s;                  // nullptr — surprise
Sneaky* right = std::addressof(s);   // the real address, bypassing operator&

// Generic code (containers, allocators) internally always uses std::addressof,
// so as not to be deceived by an overloaded operator&

In games std::addressof is rarely written directly, but it quietly works inside any generic code that manipulates object addresses, for example in custom containers and allocators, serialization systems, or object pools with placement new. Everywhere code has to take the real address of an arbitrary user type, in order to place an object there or store a pointer, std::addressof is the insurance against types that for some reason overloaded operator&.

Address Of works as a sanitation method that exists to fix the consequences of a dubious language feature, and the best way to interact with it is not to give it any work to do.

nullptr

This is a typed null pointer, which appeared in C++11 and solved the long-standing ailment of representing a "pointing nowhere" pointer. Before it, people used either the literal 0 or the macro NULL (which in C++ was usually just 0 or 0L) for this. But the problem is that 0 is first and foremost an integer, and its "null-pointer-ness" is only a special case, which gave rise to confusion between "the number zero" and "a null pointer," especially painful during overload resolution.

The classic example of the trouble is when there are two overloads, f(int) and f(char*), and the call f(NULL) intended to select the pointer version, but NULL is 0, that is, an int, so f(int) was selected, sometimes without an error and with wrong behavior. nullptr has a special type std::nullptr_t, which converts to any pointer but not to int, so f(nullptr) unambiguously selects the pointer overload. At the same time nullptr doesn't break template type deduction the way 0 does, and reads more clearly, saying directly "this is a null pointer" rather than "this is maybe zero, maybe a pointer."

A rare case where the mechanism comes at no cost, and the only "danger" is that old code is still littered with NULL and 0 in the role of pointers, and during modernization they are worth replacing. Before C++11 people tried to emulate the idiom with hand-written "nullptr" classes (that very return-type resolver with a templated operator T*), but those were half-measures. nullptr itself is essentially a standardized and polished-up version of such a class.

The predecessor idiom (a hand-written nullptr) was described by Scott Meyers and Herb Sutter even before C++11, and nullptr entered the language on a proposal by Herb Sutter and Bjarne Stroustrup precisely to eliminate the problems of NULL/0 with overloads and templates.

void spawn(int count);
void spawn(Entity* parent);

spawn(0);          // calls spawn(int), but what if you wanted parent?
spawn(NULL);       // also spawn(int)! NULL is 0 and it's an int, a bug.
spawn(nullptr);    // unambiguously spawn(Entity*), exactly what's needed

Entity* e = nullptr;        // reads clearly
if (e == nullptr) { /* ... */ }

There is no particular "game-specific" angle here, it is just general hygiene of modern C++, but in large engine codebases with thousands of pointer operations the consistent use of nullptr noticeably reduces a class of errors and improves the readability of intent. The practical advice is simple: in new code only nullptr, and when working with old code you should replace NULL/0 pointers with nullptr at the first opportunity. This is one of those little things that cost nothing but make the code a little bit safer in each of the many places where it touches pointers.

Move Constructor

The Move Constructor (and its paired move assignment operator) became a cornerstone of C++11 move semantics, allowing resources to be "moved" from one object to another instead of copying them. When the source object is something like a temporary (rvalue) or is explicitly marked as "no longer needed" (via std::move), its internals (a buffer pointer, a handle, ownership) don't have to be copied but can simply be stolen, i.e. the pointers are flipped over into the new object, and the old one is left in an empty but valid state, which turns a potentially expensive deep copy into a cheap swap of a few pointers.

Before C++11 there was no such mechanism, and returning a heavy object from a function or inserting it into a container meant a real copy (expensive), or contrivances like the ill-fated auto_ptr with its "copy-as-move." Move semantics gave the language the notion of "a source whose contents can be taken away," i.e. rvalue references (T&&) distinguish temporary/handed-over objects from ordinary ones, and the move constructor takes exactly such a reference, taking the resources and nulling out the source, while std::move is just a cast to an rvalue reference, saying "treat this object as expendable."

What you pay is the presence of an "empty but valid" state of the source after the move (you can no longer read meaningful data from it, but the destructor and assignment must work), and the mandatory marking of move operations as noexcept, without which containers (as discussed in non-throwing swap) fall back to copying. Plus, the rules for auto-generating move operations are still subtle, and declaring a destructor or copy operations suppresses the automatic generation of move, and then the "move" turns into a copy, which is easy to miss.

Move semantics in C++11 were designed by Howard Hinnant, Bjarne Stroustrup, and Dave Abrahams, and it was one of the most significant and complex changes to the language, rethinking how C++ works with values and resources. In hindsight it made "correct" what people had spent years achieving with idioms like resource return and computational constructor, and it became the foundation on which the entire modern standard library stands.

class Mesh {
    std::size_t count_ = 0;
    Vertex* verts_ = nullptr;
public:
    // Move constructor: steal the buffer
    // null out the source. noexcept is mandatory
    Mesh(Mesh&& o) noexcept : count_(o.count_), verts_(o.verts_) {
        o.count_ = 0; o.verts_ = nullptr;
        // the source is empty but valid
    }
    Mesh& operator=(Mesh&& o) noexcept {
        delete[] verts_;
        count_ = o.count_; verts_ = o.verts_;
        o.count_ = 0; o.verts_ = nullptr;
        return *this;
    }
    ~Mesh() { delete[] verts_; }
};

Mesh load();
// returning a heavy mesh is now cheap (move, not copy)
std::vector<Mesh> meshes;
meshes.push_back(load());
// the mesh is moved into the vector without copying the vertex buffer

In game development move semantics were received very well, because game code constantly moves heavy objects, like a mesh with vertex buffers, textures, sound samples, and containers with frame data. But in most engines the move (returning such an object from a loader or inserting it into a container) had already been done in their own way, and many did not abandon those mechanisms, simply adding move semantics as a wrapper over them.

Implicit conversions

A body of knowledge and cautions about the implicit type conversions that C++ performs automatically. The language generously converts one thing into another without asking: int into double, a derived class into a base, types via single-argument constructors, types via conversion operators. Sometimes this is convenient and readable, and sometimes a source of bugs, when a conversion fires where you didn't expect it, and code that shouldn't have compiled does.

The main control tools are the keyword explicit and (since C++11) explicit on conversion operators. A single-argument constructor by default defines an implicit conversion: void f(Widget) can be accidentally called as f(42) if Widget has a constructor Widget(int), and the compiler will silently build a temporary Widget from the number. By marking the constructor explicit, you forbid this implicit conversion, leaving only the explicit Widget(42). The same goes for conversion operators: explicit operator bool() works in a condition but doesn't sneak into arithmetic (as we saw in safe bool).

What you pay is balance and readability. Too many implicit conversions make the code fragile and full of surprises (chains of conversions and their interaction with overloads are especially dangerous), while too few force you to write tedious explicit casts where the conversion is obvious and safe.

The general rule here is to make single-argument constructors explicit, and to allow implicitness consciously and only where it really improves readability and is safe (for example, converting Seconds into Duration).

These rules were systematized by Meyers (several Effective C++ items about explicit and about the dangers of implicit conversions), and they evolve from standard to standard, reflecting that the community increasingly leans toward "explicit is better than implicit" as the default.

class Health {
    int hp_;
public:
    explicit Health(int hp) : hp_(hp) {}
    // explicit: no implicit int -> Health
};

void apply(Health h);
// apply(100);            // error: don't confuse a number with health
apply(Health{100});       // explicit and clear

// And here implicitness IS APPROPRIATE and improves readability
class Seconds {
    float s_;
public:
    Seconds(float s) : s_(s) {}
    // intentionally implicit: 2.0f -> Seconds naturally
};

void wait(Seconds);
wait(2.0f);
// reads as "wait 2 seconds"

In games caution with implicit conversions has historically "hurt," because game code teems with wrapper types over primitives (handles, identifiers, typed quantities, flags), and implicit conversions between them and raw int/float are often a direct path to UB and "passed an entity ID where a component ID was expected" or "mixed up time and quantity," so marking wrapper constructors explicit turns such places into compilation errors.

The practical line in engines is usually this: typed handles, IDs, and units of measurement are made with explicit constructors, so the type system catches the confusion, while the few genuinely natural conversions (for example, a scalar into a vector of identical components, or a literal into a safe wrapper) are deliberately left implicit.

This is part of the broader theme of "make incorrect code uncompilable," and the more meaningless type mixings the compiler rejects, the fewer bugs survive to runtime. That is why controlling implicit conversions through explicit has long been an everyday design tool for safe engine APIs, not an abstraction, and the habit of putting explicit by default saves a fair amount of time catching swapped-up arguments.

Recursive Type Composition (tricky)

This is a system for building types that recursively contain themselves (more precisely, instantiations of the same template) at compile time, forming tree-like or list-like structures of types. The classic example is a type list (typelist), where a "list" is represented as a "head + tail" pair, and the tail is again a list, and so on down to an empty terminator tail. Or an expression, where a node contains subtrees of the same node type, and the data structure exists not at runtime but in the type system, unfolding recursively at compile time.

Why build structures out of types? Because metaprogramming operates on types, and to process sets or trees of types (to generate code, hierarchies, storage from them), you have to structure them somehow. Recursive composition gives a way to represent a list or tree of types of arbitrary length and to traverse it recursively with metafunctions: process the head, recursively process the tail, stop at the terminator. This is, in essence, functional programming on types, with lists and recursion instead of loops.

The problem is usually the recursion depth and the compilation time. Each level of recursion is a new template instantiation, and for long lists/deep trees the compiler materializes a multitude of intermediate types, which seriously slows down the build and can hit the instantiation-depth limit.

With the arrival of variadic templates (C++11) manual recursive "head+tail" composition has largely become obsolete, and parameter packs (Ts...) now represent sets of types directly, while fold expressions (C++17) process them without explicit recursion.

All of this is the legacy of Alexandrescu and his Typelist, built exactly as recursive composition, and of Boost.MPL, where it was the foundation of metaprogramming in the 2000s, when variadic templates didn't yet exist and sets of types had to be encoded with nested pairs. The modern equivalent is std::tuple and variadic packs, which do the same thing but noticeably more easily.

// Recursive composition
// a typelist as head + tail (pre-C++11)
struct Nil {};
template <class Head, class Tail> struct TypeList {};

using Components =
  TypeList<Transform, TypeList<Physics, TypeList<Render, Nil>>>;

// Length metafunction and recursive traversal:
template <class L> struct Length;
template <> struct Length<Nil> { static constexpr int value = 0; };
template <class H, class T> struct Length<TypeList<H, T>> {
    static constexpr int value = 1 + Length<T>::value;
};

// C++11+, the same thing with a variadic pack, without manual recursion
template <class... Ts> struct Components2 {
  static constexpr int count = sizeof...(Ts);
};

In games recursive type composition in the Alexandrescu style is rarely written by hand today, but its modern forms, in the shape of variadic templates and tuples, permeate the generic infrastructure in ECS engines, which describe the sets of components a system needs with variadic packs (System<Transform, Velocity>) and unfold storage access over them.

So recursive type composition should be understood as an idea that hasn't gone anywhere, only changed form. The principle "represent a set/tree of types as data for compile-time processing" is alive and central to generic engine code, the tooling has simply become more humane, and variadic templates and fold expressions instead of recursive pairs have become the replacement for type lists.

Temporary Base Class

An old and almost completely obsolete optimization idiom, related to expression templates and the fight against temporary objects in expressions. Its goal was to represent the intermediate results of complex expressions (over matrices, vectors) as special temporary wrapper types inheriting a common base class, which allowed algorithms and operators to work uniformly with these temporaries and to avoid unnecessary copies and allocations when evaluating compound expressions.

The idea echoes expression templates, but instead of immediately creating a heavy temporary object for every suboperation, the expression is represented as lightweight temporary nodes that defer the computation. The "temporary base class" gave these nodes a common interface through which the final operation (assignment) could evaluate them in a single pass. This was one of the early attempts to solve the temporaries problem in numerical C++ before expression templates and move semantics took their final shape.

The implementation mechanism is complex and fragile and has today been almost entirely displaced by move semantics, which removed most of the pain of temporary objects (a temporary is now moved cheaply rather than copied), while copy elision eliminated many temporary objects altogether. So the temporary base class is mostly of historical interest and is a step in the evolution toward modern solutions.

The idea was born in the nineties; it comes from the same circle of ideas as Blitz++ and early work on high-performance linear algebra, where people tried to eliminate every temporary object while the language still had few tools to fight them.

// Conceptually: temporary expression nodes with a common base interface,
// so that assignment computes everything in a single pass
// without heavy intermediate matrices
struct MatExprBase { /* common element-computation interface */ };

template <class L, class R>
struct MatSumExpr : MatExprBase {
    // a lightweight temporary node instead of a full matrix
    const L& l; const R& r;
    float at(int i, int j) const { return l.at(i,j) + r.at(i,j); }
};

// Today this is done with expression templates + move semantics,
// not a separate temporary base

Boost mutant

A frankly "hacker" idiom that lets you access the same set of data through different structures, interpreting the same memory now as one type, now as another. The name refers to its origin from the depths of Boost. The classic example is a (first, second) pair that you want to access both as (first, second) and as (second, first) (reversed), without duplicating the data, by overlaying two different structure "views" onto the same memory.

This is usually implemented through a union of structures with identical layout, or through reinterpret_cast between types that the programmer is sure have the same field arrangement in memory. That is, having several "facades" over the same bytes, choosing the convenient one for a particular operation, without physically duplicating or copying the data. This allows, for example, reusing algorithms written for one layout for data in another, by "renaming" the fields through the mutant.

The problem is in the very idea and the balancing on the edge (and often beyond the edge) of undefined behavior, when accessing an object through a pointer/reference of an incompatible type violates aliasing (strict aliasing), and interpreting one structure as another through a union has subtle "active member" rules, and the compiler is entitled to optimize all of this in ways that break your expectations. The idiom works "on this compiler with these flags," but portability and correctness are in question. This is a typical "clever hack" that modern code tries to keep its distance from.

As I already said, the idea came from Boost (hence the name) more as a curious exhibit than as a recommendation, and it reflects the spirit of early Boost, where for the sake of efficiency and expressiveness people resorted to techniques that today would be considered too risky, and where the boundaries of undefined behavior were explored rather boldly.

// Conceptually: two "views" of the same bytes (risky, on the edge of UB)
struct Pair      { int first;  int second; };

struct ReversedPair { int second; int first; };
// the same layout, fields the other way around

union Mutant {
    Pair pair;
    ReversedPair reversed;
};

Mutant m;
m.pair = {1, 2};
// access through m.reversed treats the same bytes as (second=1, first=2)
// portability and correctness are very much in question

But despite the hacks, the idea has caught on in games, although most engines keep their distance from it, because it leads to bugs that show up only in a release with aggressive optimization. Mostly it is done "legally" when working with raw data, like parsing binary formats, network packets, GPU buffers, when doing a bitwise conversion between types of the same size without violating aliasing, or copying through memcpy, which the compiler optimizes perfectly.

So the boost mutant is worth knowing as an example of how not to do it, and as a reason to remember std::bit_cast, a more legal and portable replacement for cases when you really do need to look at the same bytes under different types. Such hacks with union and reinterpret_cast are, after all, a legacy of an era when there were no safe tools for this.

Multi-statement Macro

This is the idea of writing macros that expand into several statements. The problem is that the C/C++ preprocessor dumbly substitutes text without understanding the syntax, and a macro of several statements breaks in the most ordinary contexts. If the macro is do_a(); do_b();, then using it in if (cond) MACRO; without braces expands so that only the first statement falls under the if, while the second executes always, a classic bug.

The canonical cure is to wrap the macro body in do { ... } while(0). This construct groups several statements into a single syntactic block, which at the same time behaves like a single statement and correctly requires a semicolon after it (MACRO;), naturally fitting into if/else and loops. The while(0) guarantees exactly one iteration, and the compiler trivially throws it away, so there is no overhead. This is the idiomatic, recognizable way to say that "this macro is a single compound statement."

The price? Macros are evil in general, and this idiom merely makes one of their manifestations less dangerous without eliminating the root problems of the preprocessor, namely the lack of scoping, the multiple evaluation of arguments, or the invisibility to the debugger. So do/while(0) solves the narrow problem of "several statements as one" rather than anything else, but it doesn't make a macro a good idea in itself, and modern C++ (inline functions, templates, constexpr, lambdas) lets you replace most macros with normal code, which is almost always more correct.

The idea goes back to the early days of the language and the Unix/Linux kernel codebase, where do { } while(0) in macros is found everywhere, but in C++ it was inherited, and it is perhaps the most recognizable C "handshake": if you see do/while(0) around a macro body, you understand that the author came from the C world.

// Bad: breaks in an if without braces
#define LOG_AND_COUNT(msg) log(msg); ++log_count_

// if (verbose) LOG_AND_COUNT("hi");
// ++log_count_ executes ALWAYS — a bug!

// Good: do/while(0) makes the macro a single statement
#define LOG_AND_COUNT(msg) do { log(msg); ++log_count_; } while(0)

if (verbose) LOG_AND_COUNT("hi");      // now correct: both under the if
else do_nothing();                     // and else works too

In game development multi-statement macros are still encountered often, despite the general "macros are evil," because they have niches where there are few alternatives, like debugging and logging macros (which need __FILE__/__LINE__ and conditional compilation), registration macros (of components, tests, reflection), platform wrappers, asserts. In all these cases, if a macro contains several statements, do/while(0) is mandatory, otherwise it will plant a mine in someone's if. This is one of those little things, ignorance of which leads to bugs that are impossible to explain by looking at the place of use (everything looks correct there, while the bug hides in the macro expansion), and a recognizable do/while(0) is a sign that the macro's author has been through the school of late-night debugging.

Named Loop

The idea of getting labeled loops and control over nested loops, which C++ doesn't have in the form that, say, Java does (where you can write break outer; and exit the outer loop at once). In C++ break and continue act only on the nearest enclosing loop, and when there are several nested loops, exiting all of them at once or jumping to the next iteration of the outer one is nontrivial. The idiom offers ways to express this.

Historically and practically there have been several approaches, and the most direct and "honest" one was to use goto to a label after the outer loop: yes, that very goto that is usually considered bad form, but for exiting deeply nested loops it is, oddly enough, one of the cleanest options, and even Dijkstra didn't object to such limited use. The alternatives have been boolean flag variables, checked in the conditions of all the loops (verbose and cluttering the logic), or moving the nested loops out into a separate function, from which a return exits everything at once (often the cleanest option, simultaneously improving the structure of the code).

Each approach has its own price: goto frightens unprepared readers and in large quantities is genuinely harmful, although for exiting forward out of loops it is safe. Flags smear the exit logic across several places and easily contain errors, while moving into a function sometimes artificially fragments cohesive code. C++ never acquired labeled break/continue (there were proposals, but they didn't pass), so the idiom remains a set of compromises, and the choice between them is only a matter of taste and context.

The discussion around the practice of its use became part of the eternal argument about the appropriateness of goto, going back to Dijkstra's famous letter "Go To Statement Considered Harmful" all the way from 1968, and about the fact that "harmful in the general case" does not mean "harmful always."

// Search in a 2D grid with an exit from both loops at once
bool found = false;
for (int y = 0; y < height && !found; ++y)       // the flag variant
    for (int x = 0; x < width; ++x)
        if (grid[y][x] == target) {
          found = true;
          break;
        }

// The goto variant — for exiting forward this is clean and readable:
for (int y = 0; y < height; ++y)
    for (int x = 0; x < width; ++x)
        if (grid[y][x] == target)
          goto done;
done:;

// Often the best variant is simply to move it into a function and return
std::optional<Cell> find_in_grid(...) {
  ... return Cell{x,y};
  ... return std::nullopt;
}

In games nested loops are often an everyday thing, and the same traversal of 2D/3D grids (tilemaps, voxels, navigation cells), iterating over pairs of objects for collisions, searching in matrices will always be there, so the need to "exit everything at once on the first match" arises regularly.

In practice engines most often prefer the cleanest of the variants, like moving the nested loops out into a separate function with a return, because it both solves the exit problem and usually improves the readability and testability of the code, giving the search operation a meaningful name.

But the goto-to-a-label is also quite alive in engine code, where moving into a function is undesirable (for example, so as not to interfere with inlining or not to drag a lot of context through parameters), and where goto done; for exiting forward out of a hot nested loop is the most direct and fastest way without overhead. So the named loop is mostly about the conscious choice between "move into a function" (clean, the default) and "an honest forward goto" (when every little thing in the hot path matters), and about understanding that the taboo on goto is a general rule with legitimate exceptions, not a religious dogma.

Named Parameter

A mechanism that gives C++ something resembling named function arguments, which the language doesn't have (unlike, say, Python with its func(width=100, height=50)). When a function or constructor has many parameters, especially same-typed ones with default values, a positional call turns into an unreadable string like create(800, 600, true, false, true, 4, false), where it is impossible to understand what is what, and easy to mix up the order.

The most common implementation is method chaining through a builder object, where you create an intermediate parameter object whose setter methods return a reference to itself, which lets you chain them into a sequence .width(800).height(600).fullscreen(true), and then you pass this object into the function itself. Each "parameter" is named explicitly by its own method, the order becomes unimportant, and only the needed ones are specified (the rest take default values). This is the same idea as named template parameters, but for runtime function arguments.

You have to pay with the verbosity of the implementation, when you need a whole builder class with a method for each parameter, and with the small overhead of creating an intermediate object (usually eliminated by the optimizer). Plus these are not real named arguments and the compiler won't force you to specify the mandatory ones, so C++20 gave a partial alternative for aggregates in the form of designated initializers (Config{.width=800, .height=600}), which let you initialize struct fields by name, which for config structs covers most of the need.

The builder parameter is a long-standing practice, and at the library level it was perfected by Boost.Parameter, where it is one of the most frequent answers to the question "why can't you call a function in C++ with named arguments." You can't directly, but you can emulate it.

// Builder with method chaining
// the parameter names come back into the call
struct WindowDesc {
    int width_ = 1280, height_ = 720;
    bool fullscreen_ = false, vsync_ = true;
    WindowDesc& width(int w)      { width_ = w; return *this; }
    WindowDesc& height(int h)     { height_ = h; return *this; }
    WindowDesc& fullscreen(bool f){ fullscreen_ = f; return *this; }
    WindowDesc& vsync(bool v)     { vsync_ = v; return *this; }
};

Window create_window(const WindowDesc&);

// Instead of create_window(800, 600, true, false)
// readable and without confusing the order
auto win = create_window(WindowDesc{}.width(800).height(600).fullscreen(true));

// C++20 makes it simpler for aggregates
auto win = create_window({.width_=800, .height_=600, .fullscreen_=true});

In games the named parameter, in the form of builders and config structs, is an almost classic technique, because engines are full of APIs with many parameters, from window creation to render pipeline configuration and texture descriptors. They all have dozens of settings, most with reasonable default values, and a positional call would be an unreadable nightmare, where true, false, true tells the reader nothing.

And the next-generation graphics APIs (Vulkan, D3D12, Metal) are entirely built on descriptor structs filled out field by field; this is exactly named parameter in the form of a config struct, and the engines on top of them continue the same tradition. Modern game development increasingly uses C++20 designated initializers for such descriptors, because they give named initialization without writing a builder class, which is both shorter and more efficient.

So named parameter in games is mostly about config structs and descriptors, and about the choice between a classic builder (works everywhere, you can validate) and designated initializers (simpler, C++20). In both cases the goal is the same: that a call with a dozen parameters can be read and not mixed up.

Named External Argument

Close to the previous one, but a narrower idea: to make individual arguments "named" through wrapper types that carry the argument's meaning in their name. Instead of passing a bare bool or int, whose purpose at the call site is unclear, you wrap it in a small named type, and then the call documents itself: not set_visible(true) with a non-obvious true, but something where the intent is explicitly expressed by the argument's type.

Now such a "label" gives the argument meaning, and this can be a tag wrapper (Visible{true} instead of true), a strong type alias (a separate type Width over int), or a named constant flag. This is especially valuable for boolean arguments, notorious for their unreadability: a chain of true, false, true in a call is a riddle, whereas Visible::Yes, Cached::No, Async::Yes reads without consulting the documentation, and at the same time strong types prevent swapping arguments of the same base type.

You have to pay with the wrapper types, and if there are many of them, some noise from small types appears. Plus these are not full-fledged named parameters (order still matters), but rather "self-documenting arguments," but in combination with explicit and strong typing all this gives a tangible gain in both readability and safety, suppressing the class of errors "mixed up which of the three bools is responsible for what."

// Bare bools — unreadable and easy to mix up:
// player.respawn(true, false, true);   // what is what???

// Named external arguments — the type carries the argument's meaning:
enum class Invulnerable : bool { No, Yes };
enum class KeepInventory : bool { No, Yes };
enum class AtCheckpoint : bool { No, Yes };

void respawn(Invulnerable, KeepInventory, AtCheckpoint);

// The call documents itself and is protected from swapping:
player.respawn(Invulnerable::Yes, KeepInventory::No, AtCheckpoint::Yes);

In game development this is often a foundation of an engine, because game APIs abound with boolean flags and same-typed numeric parameters, and unreadable calls like spawn(pos, true, false, true, 3) are a frequent source of bugs from swapping arguments. That is why wrapping flags in named enum classes (which, as we saw in type safe enum, are also type-safe) turns such calls into self-documenting and error-resistant ones.

Strong types for quantities (separate types Health, Damage, EntityId, Seconds instead of bare int/float) is the same idea, protecting against mixing semantically different but representation-identical values, and saving a fair amount of debugging time on "mixed up an ID with a quantity" and "passed milliseconds where seconds were expected." So named external argument is a good practice of "don't pass bare primitives where a type can carry meaning," and it combines beautifully with enum class, explicit, and strong types into the overall culture of "make an incorrect call uncompilable, or at least obviously wrong when read." It is cheap, costs nothing at runtime, and noticeably improves both the readability and the reliability of the code.

Deprecate and Delete

The idea is that you can't just up and remove a function that is being used, because that would break someone else's code, and the right path is to first mark it as deprecated, so that the compiler issues a warning on use, giving users time to migrate, and then, in the next major version, to remove or explicitly forbid it.

Both mechanisms work at different stages. [[deprecated("use X")]] (a C++14 attribute, and earlier the compiler-specific __declspec_(deprecated)/__attribute__((deprecated))) marks a function as undesirable, but code with it still compiles, while the compiler warns, often with your message about where to migrate. And = delete (C++11) goes further and explicitly forbids the function, and an attempt to call it gives a compilation error. delete is used both for the final "removal" of something deprecated and (this is its main use) for forbidding undesirable operations, like copying, dangerous overloads, implicit conversions.

The price is mostly process discipline rather than a technical solution, and you need to maintain the cycle "marked → waited → removed," without rushing the removal and without forgetting about the deprecated warnings forever. = delete is more powerful than it seems, and with it you can forbid a specific overload (for example, = delete for f(double), to forbid calling an integer function with a fractional argument and not let it silently convert), which finely controls overload resolution.

Both mechanisms came in C++11 (the [[deprecated]] attribute in C++14) from long-standing compiler extensions. And = delete was designed, among other things, to replace the "private undeclared method" idiom (as in boost::noncopyable), and previously copying was forbidden with a private undeclared constructor, while now it is done with a clear = delete and an understandable error. This is part of the language's general movement toward expressing intent with explicit language means instead of idiomatic workarounds.

class Texture {
public:
    Texture(const Texture&) = delete;
    // forbid copying — with a clear error
    Texture& operator=(const Texture&) = delete;

    [[deprecated("use load_async instead of load")]]
    void load(const char* path);
    // still works, but warns
    void load_async(const char* path);
};

void bad(double);
void bad(int) = delete;     // forbid the call with int: bad(5) is a compilation error

Managed deprecation is critical for engines that have users, other teams, modders, licensees, and breaking someone else's code on every API update is unacceptable. [[deprecated]] provides a civilized migration path, where the old function is marked, the replacement is documented, users get warnings and time to switch, and only then, in a major version, is the function removed. Unreal and other large engines systematically use deprecation macros precisely for this.

= delete meanwhile has long been an everyday design tool in games for safe types, and the forbidding of copying resources and non-copyable objects (with a clear error instead of a link error), the forbidding of dangerous implicit conversions and undesirable overloads, an explicit "this operation is meaningless for this type."

This is the same cross-cutting theme of "make incorrect code uncompilable," and = delete turns a whole class of potential usage errors into compilation errors with a clear message. So deprecate-and-delete in games is both about the polite evolution of an engine's public API (deprecated) and about the hard suppression of incorrect type usage (delete), and both mechanisms have long become a standard part of an engine designer's toolkit.

Function Poisoning

This is the idiom of deliberately "poisoning" certain functions, so that their use becomes a compilation error or at least a warning, but unlike deprecate-and-delete, aimed at the evolution of your own API, poisoning is usually aimed at forbidding dangerous, unsafe, or project-banned functions. Most often someone else's, standard, or system functions, which for some reason you don't want to see in your codebase.

There are several ways, and the most direct at the tooling level is #pragma GCC poison name, which makes the compiler reject any appearance of the specified identifier (literally "poisons" the name). The second is, at the code level, to declare the forbidden function with = delete or with [[deprecated]], or to override it with your own version that doesn't compile or produces a static_assert. The goal is the same: to suppress the use of functions like the unsafe strcpy/sprintf/gets, the unwanted malloc/free (if in the project everything must go through your own allocator), printf (if direct output is forbidden), or any others that are banned in the given project.

The price for poisoning someone else's, especially standard, functions is that it is fragile and non-portable, and that same #pragma poison is not available on all compilers and can conflict with the headers themselves, where the "poisoned" name is legitimately used (poison malloc and the standard headers that use it internally will stop compiling). That is why poisoning requires care about exactly where it is enabled, and it is often confined to a special header included only in "your own" code, but not where system headers are included.

The idiom is described in works on GCC/Clang extensions, and it appeared as a tool for forbidding unsafe functions in codebases with strict safety requirements. It is part of the broader practice of enforced coding standards, as the enforcement of coding standards by compiler means rather than only by review.

// forbidden.h is included in your own code (but not where system headers are)
#pragma GCC poison strcpy strcat sprintf gets   // any use is an error

// In a project where all memory goes through your own allocator,
// you can forbid bare new:
void* operator new(std::size_t) = delete;
// forces the use of the engine allocators

// Forbidding a specific unsafe overload through = delete:
char* strcpy(char*, const char*) = delete;
// "don't use it, there's safe_copy"

In games function poisoning is usually applied on consoles, which is, in essence, all serious engines and especially certification, and they forbid unsafe C string functions (a source of buffer overflows), direct system allocations bypassing the engine allocator (so that all memory is accounted for and goes through the right pools), system calls forbidden on consoles, and slow or non-portable functions.

This is part of the "enforced cleanliness" infrastructure of a codebase, when instead of relying on every developer remembering all the prohibitions and on the reviewer catching them, the engine poisons the forbidden functions, and the compiler catches violations automatically, even before review.

For large teams this becomes a coding standard that enforces itself, more reliable than any document that everyone "has read." So function poisoning is about automatically enforcing the project's rules by compiler means, and although the technique is technically a bit fragile (especially poisoning standard names), in a disciplined codebase with a properly isolated prohibitions header it saves a lot of effort and prevents whole classes of problems that on a console could cost a certification failure.

Inner Class

The use of nested (inner) classes is needed for solving problems of encapsulation, implementing interfaces, and organizing tightly related types. In C++ a nested class is a full-fledged class in the scope of the outer one, having (with some caveats) access to its private members through an object and logically belonging to it.

There are several uses, and the first is precisely hiding implementation details, when an inner class declared in the private section is invisible from the outside and serves as a helper structure (a list node, a container iterator, a state machine's state) without cluttering the outer namespace. The second is made for implementing an interface "from inside," when the outer class doesn't inherit the interface itself (so as not to bloat its public contract and not to breed conflicts), but provides an inner class that implements the interface and has access to the outer one's internals, this is a way to "implement several interfaces" without multiple inheritance and its problems. And the third is grouping tightly related types under an owner name (Graph::Node, Graph::Edge).

What you pay is the complexity of the declaration (especially with templates: typename Outer<T>::Inner with its typename ceremonies), and it is important to remember that nesting is about scope and access, not about lifetime. An object of an inner class is not automatically bound to an object of the outer one and can live separately, while excessive nesting worsens readability. Plus there are historical subtleties about exactly what access an inner class has to the outer one's private members (the rules were refined from C++98 to C++11).

Nested classes are a long-standing part of the language, and their use (especially for container iterators and for implementing interfaces from inside) is standard practice, visible in any STL implementation, where iterator is implemented through a nested type of the container.

template <class T>
class LinkedList {
    struct Node {
        // nested, hidden: an implementation detail
        T value;
        Node* next;
    };
    Node* head_ = nullptr;
public:
    class iterator {
        // nested, public: part of the container's interface
        Node* cur_;
    public:
        T& operator*() { return cur_->value; }
        iterator& operator++() { cur_ = cur_->next; return *this; }
        bool operator!=(const iterator& o) const { return cur_ != o.cur_; }
    };
    iterator begin() { return {head_}; }
};

In games nested classes are also an everyday tool for organizing code, and custom engine containers provide nested iterator/const_iterator (as the generic container idiom requires) and hidden Node structures, while state machines hide their states as nested classes. Systems provide nested descriptor and handle types under the system's name (Renderer::CommandBuffer, Physics::Contact), and all this keeps tightly related types together, in the scope of their owner, and removes them from the global namespace, where they would only get in the way.

Rule of Zero/Three/Five

The rule of three, five, and zero is a body of recommendations about which of a class's special functions need to be defined together, so that the class manages its resources correctly. The rule of three (the C++98 era) states that if you needed to define at least one of the trio "destructor, copy constructor, copy assignment operator," then you almost certainly need all three, because their presence signals manual management of a resource, and the compiler-generated versions of the other two will do it wrong (a shallow copy of a pointer leads to a double free).

The rule of five is the extension of the trio into the C++11 era, and to the destructor and copy operations were added the move constructor and the move assignment operator, and now "all or nothing" extends to the whole quintet. If a class manages a resource manually and you define a destructor with copying, you should also define moving, otherwise it will either not be generated (and moving will silently become copying, losing performance) or be generated incorrectly.

And the rule of zero turns everything above on its head, and the best way to satisfy the rules of three and five is not to write any of these functions at all. Then each member of the class itself correctly manages its own lifetime (this is std::vector, std::string, std::unique_ptr, not raw pointers and handles), and the compiler-generated versions of all five functions automatically turn out to be correct.

The rule of three is a fairly old rule, which Marshall Cline and others recorded back in the nineties, while the rule of five took shape with the move semantics of C++11, and the rule of zero was formulated and popularized by R. Martinho Fernandes in 2012, and it was picked up by the Core Guidelines as the preferred approach.

// Rule of five: a class with a raw resource must define all five
class Buffer {
    float* data_; std::size_t size_;
public:
    ~Buffer() { delete[] data_; }                         // 1
    Buffer(const Buffer&);                                // 2
    Buffer& operator=(const Buffer&);                     // 3
    Buffer(Buffer&&) noexcept;                            // 4
    Buffer& operator=(Buffer&&) noexcept;                 // 5
};

// Rule of zero: write nothing, and the members manage themselves, and that's correct
class Mesh {
    std::vector<float> vertices_;
    // copies, moves, cleans up itself
    std::string name_;
    // no destructor, no copy/move — the compiler does everything correctly
};

Most Vexing Parse

This is not a technique but a C++ syntax trap, knowledge of which is mandatory in order not to lose an hour to a mysterious error. The essence is in a grammar rule: if something can be parsed both as a function declaration and as an object definition with initialization, the standard prescribes parsing it as a function declaration. That is, when you write what looks to you like creating an object, the compiler sometimes sees a function declaration, and this happens in the most innocent-looking places.

The canonical example: you want to create an object Widget w(Gadget());, passing a temporary Gadget into the constructor. But the compiler parses this line as a declaration of a function w that returns a Widget and takes... a pointer to a function with no arguments returning a Gadget. No object is created, and a function is declared, while the subsequent use of w as an object gives a cascade of incomprehensible errors, none of which say a word about w actually being a function. Even more treacherous are empty parentheses: Widget w(); is no longer "create a Widget with the default constructor" but a declaration of a function w returning a Widget.

This is cured by resolving the ambiguity in favor of "this is definitely an object" and adding extra parentheses around the argument (Widget w((Gadget()));), which make parsing it as a function impossible. The modern and far cleaner way, and you should use brace list-initialization (Widget w{Gadget{}}; or Widget w{}; for the default one), which in principle cannot be parsed as a function declaration and therefore kills the most vexing parse outright.

The name "most vexing parse" was coined by Scott Meyers in Effective STL, and it caught on instantly, because it precisely conveys the feeling that this is the most vexing of all the ways in which C++ grammar can deceive you.

struct Gadget {};
struct Widget { Widget(Gadget); Widget(); void use(); };

// The trap: these are FUNCTION DECLARATIONS, not object creations!
Widget a();              // a function a() returning Widget, not an object!
Widget b(Gadget());      // a function b(pointer to function), not an object!
// a.use();              // error: a is a function, it has no .use()

// Solutions:
Widget c;                // an object (no parentheses) — default constructor
Widget d{};              // an object (braces) — unambiguous
Widget e{Gadget{}};      // object + braces kill the ambiguity

Pass-key

This is an elegant way to give access to a specific method of a class strictly to certain other classes, without making them full-fledged friends and without opening up everything private to them. It solves the same problem of "friendship in C++ is too coarse" as Attorney-Client, but with a different, in many cases cleaner technique. The method stays public, but requires in its arguments a "key," an object of a tiny type that only the permitted class can construct.

An empty key class with a private constructor is created, and it declares as its friends exactly those classes that are allowed to use it. The target public method takes this key as a parameter, and since only a friend of the key can create the key, only the one who can construct the key, that is, only the permitted classes, can call the method.

Everyone else sees the public method but cannot create the key to call it and gets a compilation error. Friendship thereby becomes tight, and the "golden key" opens access to one specific method for specific classes, rather than to the whole class at once.

The advantage over an ordinary friend and over Attorney-Client is precision and the absence of transitivity: while an ordinary friend sees absolutely everything private, the pass-key opens exactly one method, and you don't need a separate intermediary class with forwarding, a tiny key type is enough.

The idiom got its name and popularity in the C++ community of the early 2010s (it was described in discussions on Stack Overflow and in blogs under the names "passkey" and "pass-key"), as a lighter alternative to attorney-client for the frequent case of "give access to a constructor or a single method only to a factory or a manager."

class Entity;

class EntityKey {
    // the key: only EntityManager can create it
    EntityKey() = default;
    friend class EntityManager;
};

class Entity {
public:
    // the method is public but requires a key
    // which means only a friend of the key can call it
    void set_id(int id, EntityKey) { id_ = id; }
private:
    int id_ = 0;
};

class EntityManager {
public:
    void assign(Entity& e, int id) { e.set_id(id, EntityKey{}); }
    // is able to create the key
};
// any other code: e.set_id(5, EntityKey{});
// error, the key's constructor is inaccessible

In games the pass-key fits well onto the common pattern "only the manager/factory has the right to this operation." Only EntityManager should assign IDs to entities; only ResourceManager may change a resource's internal state, or only the loading system may call an object's "raw" initializer. The pass-key expresses this at the type level, without making the manager an all-encompassing friend of every class and without flinging open all encapsulation for the sake of one trusted operation.

This works around the classic problem "a private constructor isn't friends with make_unique." So the pass-key is a practical tool for managers and factories, where you need to carefully restrict who has the right to sensitive operations, and it often turns out cleaner and lighter than both a bare friend and a full-fledged Attorney-Client.

Defaulted Comparisons and the <=> operator (Spaceship)

Defaulted comparisons is a C++20 feature for asking the compiler to generate the comparison operators for you, and the idiom "don't write comparisons by hand if they are trivial" was a historical pain, and to make a type comparable you had to define up to six operators (==, !=, <, >, <=, >=), by hand at that, consistently and without errors, and so for every comparable type.

These were mountains of boilerplate, in which it was easy to make a mistake (to write < inconsistently with ==), and each one bloated the class with its implementation. The <=> operator (formally "three-way comparison," colloquially "spaceship" for its appearance) and the defaulted == solve this in that you write auto operator<=>(const T&) const = default; and the compiler generates all the ordering operations (<, >, <=, >=), comparing the members lexicographically in declaration order.

Separately, bool operator==(const T&) const = default; gives == and !=, so one or two lines instead of six hand-written operators, and they are guaranteed to be consistent with each other. <=> moreover returns not a bool but a special "comparison category" type (strong_ordering, weak_ordering, partial_ordering), precisely expressing what kind of ordering this is. The <=> operator was designed by Herb Sutter (together with Jens Maurer) and introduced in C++20 precisely to put an end to comparison boilerplate, one of the most tedious chores in C++.

struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;
    // gives <, >, <=, >= all at once
    bool operator==(const Version&) const = default;
    // gives == and !=
};

Version a{1, 2, 0}, b{1, 3, 0};
bool older = a < b;
// works: lexicographically by major, minor, patch
bool same  = a == b;
// also works

// std::sort, std::set, std::map<Version,...>
// now work without hand-written operators

What to do with all this

Looks like I have enough material for a sequel to Game++ :) But seriously, several cross-cutting lines emerge that are more important than any individual idiom, idea, or mechanism.

A good half of these idioms are crutches. Crutches that programmers carved out by hand because the language lacked a feature, while the work had to be done today. Safe bool was waiting for explicit operator bool. Resource return and computational constructor were waiting for move semantics. Int-to-type and enable-if were waiting for if constexpr and concepts. Type generator was waiting for alias templates. The nullptr wrappers were waiting for nullptr. And when the language finally caught up, the idiom collapsed into a single line or dissolved into the syntax, leaving behind only an archaeological layer in old code. These idioms are worth reading in the same double sense as the console memory architectures from one of my articles, as a historical artifact explaining why ancient code looks exactly the way it does, and as a living technique where the old standard is still in service.

The second line is about the eternal trade-off between flexibility and speed, and game development takes a very definite side in it. Virtual functions, type erasure, polymorphic value types, acyclic visitor: all of these buy runtime flexibility at the cost of indirection, allocations, and cache misses, and all of these the engines deliberately push out of themselves into tools and editors, not letting them into the engine core. And there what rules is CRTP, traits, tag dispatching, policy-based design, concrete data types, and a small army of techniques that move decisions from runtime to compile time, so that the processor doesn't do anything unnecessary.

And the last one is the rule "make incorrect code uncompilable." Non-copyable, = delete, explicit, type safe enum, named external argument, strong types, function poisoning, checked delete: they are all about having the error caught by the compiler rather than by runtime, QA, or certification. In a large team where the code is written by dozens of people of varying experience, this is perhaps the most valuable category of idioms, and every stupidity forbidden at the type level is a bug that never got born in production.

All of these are ways to tell the compiler more about our intentions, so that it does more for us and less is left at runtime, and some of them the language has already made unnecessary, while some will outlive us all.

P.S. @Boomburum Will you give me an achievement for the longest article on Habr in 20 years? Because I came up just a touch short here.

P.P.S. I think I broke the Habr editor, and now it refuses to save such a long text.

P.P.P.S. Oh wait, no, it saved after ten minutes... Phew...

P.P.P.P.S. Habr-cake! Thanks to everyone who sends in corrections.

If you liked it, drop by https://t.me/game_cpp_book, I write rarely

← All articles