Step 11 · Project orientation · C1

C++ language, by example

Use the codebase map to locate these examples, then connect the objects and ownership from the beginner lessons to the C++ used in the project. Read the syntax before building and following the loader. The Read section contains project excerpts, often with omitted declarations; the Try section provides a complete standalone program. Revisit individual constructs as you encounter them in later source traces.

Before this page: Objects, owners and borrowed memory · The codebase map & the toolbox. View the learning route.

Read

C++ here is not trying to be clever. It is trying to do three things a garbage-collected language never asks of you: control the exact bytes of a struct, keep objects alive for a precise lifetime, and generate code at compile time instead of runtime.

The reading surface

Nearly everything is wrapped in a nested namespace, written with the C++17 shorthand. The nesting is organisational, not semantic — you can usually read the innermost name and ignore the rest:

src/kernel/memory.h
namespace Libs::LibKernel::Memory {   // Libs { LibKernel { Memory { ... } } }
  ...
}

using aliases replace typedef and are everywhere. The ones that look alarming are function-pointer aliases — read them right to left:

src/kernel/memory.h
using callback_func_t = void (*)(uintptr_t addr, size_t size);
//  "callback_func_t" is a pointer to a function that takes
//   (uintptr_t, size_t) and returns void.

At namespace scope, static gives a function internal linkage; an anonymous namespace can do the same. Internal linkage does not prevent guest access through a registered function pointer. To understand an HLE entry point, check both its linkage and its LIB_FUNC registration. Absence of static alone does not make a function a public API.

`auto` — the default spelling

auto deduces the type from the initialiser. The tree writes const auto for anything read-only, const auto& to avoid copying, and auto* when the initialiser is a pointer. It never deduces a reference unless you write the & yourself:

src/loader/runtimeLinker.cpp
const auto aligned_stack_top = reinterpret_cast<uintptr_t>(stack_top) & ~static_cast<uintptr_t>(0x0f);
const auto guest_rsp         = aligned_stack_top - 2u * sizeof(uintptr_t);
src/libs/vaContext.h
auto* addr = reinterpret_cast<T*>(ptr);   // auto* keeps the pointerness

Think of auto as TypeScript's let x = ... where the type is always inferred — except there is no : Type annotation syntax for it. When a declaration says const auto&, read "a reference that I will not copy and cannot modify".

`constexpr` — computing at build time

constexpr marks a value (or function) that can be evaluated when the binary is built. src/loader/elf.h opens with a wall of them, because the ELF format's magic numbers never change:

src/loader/elf.h
constexpr char    ELFCLASS64    = 2;
constexpr int     EM_X86_64     = 62;
constexpr int     EV_CURRENT    = 1;
constexpr Elf64_Word PT_LOAD     = 1;
constexpr Elf64_Sxword DT_NEEDED = 0x00000001;
// a compile-time database of the format, used to parse game modules

Its partner is static_assert, which fails the build if the condition is false. The tree uses it as a contract on struct sizes — the ultimate guard against the compiler inserting padding you did not ask for:

src/kernel/memory.h
struct VirtualQueryInfo { ... };

static_assert(sizeof(VirtualQueryInfo) == 72, "VirtualQueryInfo struct size is incorrect");

Then the genuinely modern part. src/common/subsystems.h uses consteval (a function that must run at compile time), if constexpr (compile-time branching) and a requires clause (asking "does this type have a member?"):

src/common/subsystems.h
template <typename Lifecycle>
static consteval Callback ShutdownCallback() {
    if constexpr (requires { Lifecycle::shutdown; }) {
        return Lifecycle::shutdown;
    }
    return nullptr;
}

This is a type that may or may not provide a shutdown member; the code adapts at compile time instead of runtime. It feeds the wider pattern of a Lifecycle descriptor — a struct of static constexpr members that describes a subsystem to a registry:

src/kernel/memory.h
struct Lifecycle {
    static constexpr const char* name       = "Memory";
    static constexpr auto        initialize = Libs::LibKernel::Memory::Initialize;
    static constexpr auto        shutdown   = Libs::LibKernel::Memory::Shutdown;
};

static constexpr auto initialize = SomeFunction; stores a function pointer as a compile-time constant. The pointer is a compile-time constant; the subsystem registry still records successful lifecycle entries at runtime for reverse-order shutdown.

Templates — code that writes code

A template is a recipe instantiated at compile time for each combination of arguments. Three shapes appear constantly:

1. Function templates that deduce their own sizes. In OpcodeTable.h, the array size is captured by passing the array by reference — const Entry (&table)[N] — so FindOpcode works for any table without the caller saying how long it is:

src/graphics/shader/recompiler/frontend/decode/OpcodeTable.h
template <typename Entry, size_t N>
constexpr const Entry* FindOpcode(const Entry (&table)[N], uint32_t encoding) {
    for (const auto& entry: table) {
        if (entry.encoding == encoding) {
            return &entry;
        }
    }
    return nullptr;
}

2. Class templates with non-type parameters. Template arguments do not have to be types — they can be numbers. BitArray<N> is a fixed-size bit set, and the MultiLevelPageTable for the texture cache is configured entirely by numbers:

src/graphics/host_gpu/renderer/cache/multiLevelPageTable.h
template <typename EntryT, size_t PageBits = 20, size_t AddressSpaceBits = 40,
          size_t FirstLevelBits = 10>
class MultiLevelPageTable final {
    static_assert(AddressSpaceBits < 64);
    static_assert(PageBits < AddressSpaceBits);
    ...
};

3. Explicit specialisation. When one combination needs a hand-written version. vaContext.h has a bespoke reader for 32-byte float4 arguments that the generic path cannot handle:

src/libs/vaContext.h
template <>
inline VaFloatX4 VaArg_reg_save_area_fp<VaFloatX4, 32>(VaList* l) {
    // two 16-byte registers stitched into one float4
    ...
}

A function template describes a family of functions; a class template describes a family of types. Arguments select a specialization. Instantiation errors often include a chain of call sites: start at the first relevant diagnostic and follow that chain to the unsupported operation, rather than assuming every template error is located at the caller.

Move semantics and perfect forwarding

After you understand the B3 owner transfer, read src/common/uniqueFunction.h, a move-only, type-erased callable. Type erasure stores different callable types behind one interface. Read the constructor, owned implementation object and invocation path separately:

src/common/uniqueFunction.h — the interesting half
template <typename Result, typename... Args>            // variadic template
class UniqueFunction {
    ...
    UniqueFunction(UniqueFunction&&) noexcept            = default;  // movable
    UniqueFunction(const UniqueFunction&)                = delete;   // not copyable

    template <typename Function>
    UniqueFunction(Function&& function)
        : m_callable(std::make_unique<Callable<std::decay_t<Function>>>(
              std::forward<Function>(function))) {}
    ...
};
  • Move-only. The copy constructor is = delete, the move is defaulted. A std::function copies; a UniqueFunction cannot, which is exactly the guarantee you want when a closure owns a resource (a buffer, a fence).
  • std::decay_t<Function> strips references and const from the deduced type so the stored member has a concrete, plain type.
  • std::forward re-applies the argument's original value category — lvalue stays an lvalue, rvalue stays an rvalue — so nothing is copied unnecessarily. std::move is the "I am done with this" cast; std::forward is the "pass it along as it came" cast.

Distinguish two meanings of &&. A parameter such as UniqueFunction<void>&& is an ordinary rvalue reference: it accepts an rvalue, and the function may move from it. In a deduced function template, T&& with a cv-unqualified template parameter is a forwarding reference: it accepts lvalues as well as rvalues. The constructor above uses that second form. A named variable is an lvalue expression even if its type is an rvalue reference; std::forward<T> restores the caller's value category. Neither && nor std::move alone performs an ownership transfer. See the language rules for forwarding-reference deduction and the runnable example below.

RAII — the object owns the resource

There is no garbage collector. A resource (memory, a Vulkan handle, a file) is owned by an object whose destructor releases it. std::unique_ptr is the default owner; std::shared_ptr is used when ownership genuinely must be shared.

src/graphics/host_gpu/renderer/cache/multiLevelPageTable.h
auto& bucket = m_first_level[FirstLevelIndex(page)];
if (bucket == nullptr) {
    bucket = std::make_unique<Bucket>();   // allocated lazily, freed by the table's destructor
    ++m_allocated_buckets;
}

And here is the deliberate use of shared_ptr, from the event-queue code (also covered in HLE libraries): a queue can be destroyed while another thread is blocked in it, so it is pinned with a shared reference for the duration of the operation:

using KernelEqueueRef = std::shared_ptr<KernelEqueuePrivate>;
[[nodiscard]] KernelEqueueRef KernelPinEqueue(KernelEqueue eq);

RAII ties cleanup to an owning object's lifetime. That owner can be a local variable, a member or a dynamically allocated object. unique_ptr expresses exclusive ownership; several shared_ptr handles can share ownership until the final strong owner releases the managed object. Returning a unique_ptr gives the caller an exclusive owning handle; returning a shared_ptr may share ownership with handles that already exist. Follow the actual handle lifetimes, including deferred work and ownership cycles, rather than assuming closing one stack scope releases every resource.

Casts — four kinds, one habit

These cast spellings recur in this tree:

CastMeansIn this tree
static_castConvert between compatible typesinteger width changes: static_cast<uint32_t>(value), static_cast<uint32_t>(m_data.size())
reinterpret_cast"Treat these bytes as this type" — uncheckedpointer ↔ integer: reinterpret_cast<uintptr_t>(stack_top), reinterpret_cast<T*>(ptr). The workhorse of the whole project.
const_castDrop constrare; the signal is almost always "design smell nearby"
C-style (Type)xwhatever compilesstill common in src/libs/, e.g. (uint64_t)init_param in log lines

The one habit to build: reinterpret_cast is a promise with no runtime check. A static_assert(sizeof(...)) checks size only. It does not prove alignment, offsets, object lifetime, aliasing validity or that an address is mapped. When you see one, ask: what fixed layout is being relied on, and where is the assert?

Guest addresses are just integers

This is the single most important mental model for the codebase, and it is not a C++ feature — it is how C++ is used here. The game's memory lives in a 48-bit guest address space that the emulator lays out itself. Through most of the code, a guest address is an ordinary integer:

src/kernel/memory.h — the API surface
void     WriteBacking(uint64_t vaddr, const void* data, uint64_t size) noexcept;
void     InvalidateMemory(uint64_t vaddr, uint64_t size);
int      KYTY_SYSV_ABI KernelMprotect(const void* addr, size_t len, int prot);
uint64_t AllocateProgramMemory(uint64_t search_addr, uint64_t size, ...);

A real host pointer appears only at the boundary — when something must be executed or dereferenced. There the two worlds meet through reinterpret_cast:

auto* func = reinterpret_cast<entry_func_t>(addr);   // guest address → host function pointer
const auto aligned_stack_top =
    reinterpret_cast<uintptr_t>(stack_top) & ~static_cast<uintptr_t>(0x0f);

So the same eight bytes are a number in one function and a pointer in the next. For each vaddr or uint64_t argument, establish whether it represents a guest virtual address, host address, offset or plain integer. Guest mappings still occupy the host process; the integer type alone does not prove the domain.

Raw memory and fixed layouts

An emulator constantly reinterprets bytes, so the tree is disciplined about layout. Four tools come up over and over:

1. #pragma pack(1) — turns off padding. The compiler inserts alignment padding between struct members; when the struct must mirror a file format or a wire layout, that padding is poison. src/loader/jit.h wraps its machine-code structs in #pragma pack(1), and vaContext.h does the same for its register-save area.

src/libs/vaContext.h
#pragma pack(1)

struct VaList {
    uint32_t gp_offset;
    uint32_t fp_offset;
    void*    overflow_arg_area;
    void*    reg_save_area;
};

#pragma pack()

2. Bitfields — flags packed into a single word by declaring a bit width:

src/kernel/memory.h
struct VirtualQueryInfo {
    ...
    uint32_t is_flexible  : 1;
    uint32_t is_direct    : 1;
    uint32_t is_stack     : 1;
    uint32_t is_pooled    : 1;
    uint32_t is_committed : 1;
    uint32_t is_gpu_prt   : 1;
    uint32_t amm_usage    : 1;
    uint32_t reserved     : 1;
    char      name[KERNEL_MAXIMUM_NAME_LENGTH];
    uint8_t   gpu_mask_id;
    uint8_t   reserved2;
};

static_assert(sizeof(VirtualQueryInfo) == 72, "VirtualQueryInfo struct size is incorrect");

C++ bitfield allocation and ordering depend on the implementation. Guest and host code need not use the same compiler or ABI. An internal host representation can use bitfields under the project's compiler assumptions; guest-visible layouts must be checked against their actual binary contract. static_assert(sizeof(T) == N) proves only total size, not field offsets, bit positions or endianness. Use explicit masks/shifts when decoding format words, and verify required offsets and behavior separately.

3. Anonymous unions — several views of the same bytes, selected by a tag. The memory-pool batch entry is one 32-byte record whose meaning depends on op:

src/kernel/memory.h
struct KernelMemoryPoolBatchEntry {
    uint32_t op;
    uint32_t flags;
    union {
        struct { void* addr; uint64_t len; uint8_t prot; uint8_t type; } commit;
        struct { void* addr; uint64_t len; } decommit;
        struct { void* dst;  void* src;  uint64_t len; } move;
        ...
        uintptr_t padding[3];
    };
};

4. offsetof — the byte offset of a member within a struct. vaContext.h uses it to build a hand-rolled variadic-argument reader by offset arithmetic on the register save area.

src/libs/vaContext.h — inside the VA_CONTEXT macro
(ctx).va_list.gp_offset         = offsetof(VaRegSave, gp);
(ctx).va_list.fp_offset         = offsetof(VaRegSave, fp);

Follow std::vector, direct magic_enum calls, std::filesystem::path and common/alignment.h in current code. Profiler macros create native tracy::ScopedZone objects.

Attributes — messages to the compiler

Attributes are contracts with the compiler, not runtime features. The ones that recur:

AttributePromiseSeen at
[[nodiscard]]"The return value must not be ignored" — the compiler warns if you drop itmemory.h and common/archive.h
[[maybe_unused]]"I know I never use this; do not warn"the thread_local flag in libs.h
noexcept"This function does not throw"memory.h
__attribute__((format(printf, a, b)))check printf-style arguments at compile time, like the C runtime doesKYTY_FORMAT_PRINTF in common.h
noreturn, naked, returns_twice, sysv_abi/ms_abithe ABI and machine-code edge — the subject of the next topicruntimeLinker.cpp, libKernel.cpp, abi.h

The preprocessor as a code generator

Modern C++ avoids macros, but this codebase uses them deliberately in three places — and you must read them to read the libraries:

1. Special-member boilerplate. Deleted copy/move constructors for a whole class in one line:

src/common/common.h
#define KYTY_CLASS_NO_COPY(name)                                                       \
public:                                                                                 \
    name(const name&)                = delete;                                         \
    name& operator=(const name&)     = delete;                                         \
    name(name&&) noexcept            = delete;                                         \
    name& operator=(name&&) noexcept = delete;

2. The HLE registration DSL. The whole of src/libs/ is registered with a handful of macros (covered in HLE libraries and animated below). Notice the # operator in LIB_ADD — it stringifies its argument at compile time, turning a function name into a log label with no reflection machinery:

src/libs/libs.h
#define LIB_ADD(n, f, t)                                                       \
    {                                                                          \
        Loader::SymbolResolve sr {};                                           \
        sr.name  = n;  sr.library = g_library;  sr.module = g_module;          \
        ...
        auto        func     = reinterpret_cast<uint64_t>(f);                  \
        const char* dbg_name = "" #f;        // "function_name" as a string \
        s->Add(sr, func, dbg_name);                                            \
    }

#define LIB_FUNC(n, f) LIB_ADD(n, f, Loader::SymbolType::Func)

3. ABI shims that must be spelled identically at every call site — VA_ARGS expands to the System V argument list, VA_CONTEXT to the code that captures it into a struct.

Reading a macro: expand it in your head, pasting the arguments into the body. The \ at line ends just means "this macro continues on the next line". __func__, used by PRINT_NAME(), names a predefined local character array containing the current function name. __FUNCTION__ is a compiler extension, not a template.

And the thread_local in LIB_NAME is worth calling out: each translation unit gets its own per-thread flag, which is how PRINT_NAME_ENABLE(true) can turn on tracing for one module on one thread without flooding the log from everywhere else.

The modern STL you will meet

Familiarity with a handful of headers covers most of the tree:

FeatureWhat it isSeen at
Structured bindingsdestructuring a pair/tuple into names — like JSauto [it, inserted] = m_free.emplace(vaddr, size); in memoryAddressSpace.inc
std::optional<T>a value that may be absent — like T | nullguestInstructionPatcher.cpp, profiler.h; read with .has_value() / .value()
std::span<const T>a non-owning view of contiguous data — like a typed sliceguestInstructionPatcher.h, shader.h
std::from_charsfast, allocation-free number parsingmain.cpp, gamePatch.cpp
Lambdasanonymous functions, passed as comparators/callbacksstd::ranges::find_if in the program cache
<bit> helpersbit tricks: std::countr_zero, std::countl_onebitArray.h scanning for set-bit runs
std::atomic, std::mutexthread-safe counters and locksagc.cpp, ajm.cpp, memoryAddressSpace.inc

Coming from TypeScript / JavaScript

JS / TSC++ in this tree
const x = f()const auto x = f()
interfacea class with pure virtual methods — see CallableBase in uniqueFunction.h
class with methodsa struct (everything public) is the default here
arrow functionlambda: [capture](params) { ... }
null, optional chainingnullptr; std::optional + .has_value()
destructuring const [a, b] = pairauto [a, b] = pair
.map() / .filter()std::transform and friends — or just a loop. STL is verbose; loops are idiomatic here too.
anytype erasure (as in UniqueFunction), or an integer + tag (as in the packed unions)
exceptionslargely avoided; results are return codes, EXIT_IF asserts, or log lines
garbage collectorRAII and explicit ownership policies; borrows do not keep objects alive.
One habit to break

In JavaScript, const prevents rebinding a variable; it does not freeze the referenced object. In C++, const T& restricts mutation through that reference, with qualifications such as mutable members and separately referenced objects. Other valid aliases may still modify a non-const object. Constness does not establish ownership, extend lifetime or provide synchronization.

Learn more · C++ for this codebase
  • Reference
    cppreference.comThe dictionary. For the constructs above, start with the pages on constexpr, templates, move constructors, and the static_assert declaration. Better for looking things up than for learning.
  • Course
    Learn C++A sequential language course. Use the named lessons on pointers and smart pointers and move semantics; these links were checked on 7 October 2026.
  • Book
    Effective Modern C++Scott Meyers. Use its discussions of move semantics, forwarding references and smart pointers as deeper reading after the runnable example. The book targets C++11/14; this project also uses later language features.
  • Tool
    C++ InsightsPaste a snippet and watch templates, auto and if constexpr expand into the code the compiler really sees. The fastest way to make the "factory of functions" idea concrete.
  • Spec
    C++ Core GuidelinesThe why behind the conventions this tree follows, including the RAII and smart-pointer rules that make lifetimes traceable.
Watch

The macro you will meet on your first change, taken apart: the one line that registers an HLE function, and what it becomes.

Try

A complete forwarding and ownership example

A named integer is an lvalue; the literal 7 is an rvalue. Predict which overload category() selects after the forwarding wrapper. Then follow the shared object after one handle resets. The final operation copies a float representation with std::bit_cast; it does not dereference an incompatible pointer.

#include <bit>
#include <cstdint>
#include <iostream>
#include <memory>
#include <utility>

const char* category(int&) { return "lvalue"; }
const char* category(int&&) { return "rvalue"; }

template<class T>
const char* forward_category(T&& value) {
    return category(std::forward<T>(value));
}

int main() {
    int number = 7;
    std::cout << forward_category(number) << '\n';
    std::cout << forward_category(7) << '\n';
    auto first = std::make_shared<int>(number);
    auto second = first;
    first.reset();
    if (!second || *second != 7) return 1;
    std::cout << "shared value=" << *second << '\n';
    static_assert(sizeof(float) == sizeof(std::uint32_t));
    const float value = 1.0f;
    const auto bits = std::bit_cast<std::uint32_t>(value);
    if (std::bit_cast<float>(bits) != value) return 1;
    std::cout << "bit pattern round trip preserved\n";
}

Configure once using B1, then run:

cmake --build _Build/curriculum/beginner --target cpp_idioms
ctest --test-dir _Build/curriculum/beginner -R '^beginner_cpp_idioms$' --output-on-failure

Expected output:

lvalue
rvalue
shared value=7
bit pattern round trip preserved
Exercise and worked answer

Replace std::forward<T>(value) with value. Predict the change before rebuilding: both calls select the lvalue overload because value is a named expression. The output test should fail on its second line. Restore forwarding and verify the original output. Explain separately why resetting first does not destroy the shared integer while second owns it.

Do

These checkpoints all end in "and then read the source" — so you can verify each explanation against the selected checkout and its callers.

Checkpoint