C++ Module 22 – C++ for game development
C++ in Unreal, Godot and custom engines
How Unreal Engine, Godot and custom game engines use C++: reflection macros and generated code, garbage collection, and builds without exceptions or RTTI.
What you will learn
- Explain what game engines add to standard C++ and what that description drives
- Recognise how Unreal Engine and Godot register classes and their members
- Build a property table with macros, and name the mistake it cannot catch
- Compare a table kept by hand, a generated one and one built by C++26 reflection
- Reproduce a pointer a garbage collector cannot see, and replace RTTI and exceptions
Before you start
What you’ll learn · Before you start
What you will learn
- Explain what game engines add to standard C++ and what that description drives
- Recognise how Unreal Engine and Godot register classes and their members
- Build a property table with macros, and name the mistake it cannot catch
- Compare a table kept by hand, a generated one and one built by C++26 reflection
- Reproduce a pointer a garbage collector cannot see, and replace RTTI and exceptions
Before you start
On this page
A game editor shows a player’s name, health and speed in a panel, a save file stores them, and a console command can
change them, yet none of that code was written for the Player class. It all reads a description of the class that
the engine keeps, something standard C++ long lacked. C++ in Unreal is written with macro: A name defined with #define that the preprocessor replaces by its text wherever it appears later in the file, as SQ(a + 1) becomes ((a + 1) * (a + 1)). A macro has no type and no scope, and its arguments are pasted as text. More in “C++ preprocessor and feature-test macros”
such as UCLASS, UPROPERTY and UFUNCTION, which a separate program, the Unreal Header Tool, reads before the
compiler runs, to generate the code that registers each class. Godot’s C++ bindings, godot-cpp, register a class with
the GDCLASS macro and a _bind_methods function. Custom engines keep a table of their own, written by hand or
generated.
Engines also bring their own containers, and decide module by module whether exceptions and run-time type information (RTTI): The description of its own type that the compiler keeps for each class with virtual functions, which dynamic_cast's checks and typeid read while the program runs. With -fno-rtti GCC and Clang leave it out and reject those uses. More in “C++ casts and run-time type information (RTTI)” may be used. C++26 reflection lets the compiler build such a table from the class itself. You will run a registry that shows a class it never names, and see the mistake no such table catches.
Every engine keeps a description of its classes
Game engines are built with ordinary C++ compilers, and each adds that description its own way. Called reflection, it drives the editor’s property panels, saving and loading, scripting, network replication and, in Unreal Engine, garbage collector: A part of some languages' run-time systems that finds memory a program no longer uses and frees it automatically, at moments the program does not choose. Standard C++ has none: objects are destroyed at points the code determines. More in “What C++ is used for today and how to learn it”. The three kinds of engine build it, and free their objects, in different ways:
| Engine | Classes made visible by | Objects freed by |
|---|---|---|
| Unreal Engine | Macros a header tool reads | A garbage collector |
| Godot (godot-cpp) | GDCLASS and a binding function | Reference counts, or by hand |
| Custom engines | A table, by hand or generated | Pools, handles or counts |
Follow the three paths into the compiler. Each ends in the same description, the only thing the engine’s generic code reads:
Three ways a class's description is built, and what the engine does with it
Text description of the diagram
The diagram shows three paths into the C++ compiler, which all end in the same class description.
- Unreal's path: markers such as UCLASS and UPROPERTY sit in the class. First, a header tool, the Unreal Header Tool (UHT), reads them and writes generated C++; the compiler then compiles the class and the generated code together.
- A list in code, written by hand, such as the REFLECT macros of this lesson or Godot's _bind_methods, goes to the compiler with the class.
- C++26 annotations on the members are read by the compiler itself, with reflection.
From the compiler comes the class's description, its properties with their types and flags, which the engine uses for editor panels, saving, scripts, networking and garbage collection.
Not affiliated
Based on public information about Epic Games. MySmartCoPilot is not affiliated with Epic Games, and Epic Games’s real Unreal Engine internals may differ from what this lesson describes.
Checkpoint
Check: What engines add to standard C++
Unreal generates the registration code, Godot writes it by hand
C++ in Unreal: macros that a tool reads
Epic’s documentation calls the step a preprocessing of the headers, done by the UnrealHeaderTool, or UHT. A class
derived from UObject carries UCLASS() above it and GENERATED_BODY() inside it, and its header ends its includes
with the generated header, such as MyObject.generated.h. A function or property without such a marker is missing
from the class’s description, and the UObject system built on that description provides garbage collection,
serialization, editor integration, run-time type information and network replication. C++ in Unreal follows other
rules too: objects are never created with new, the coding standard says to avoid standard containers and strings
except in interop code, and each module’s build rules switch exception handling and run-time type information on or
off.
Godot: classes registered in _bind_methods
godot-cpp is the Godot project’s own C++ binding for its GDExtension system. Unlike a C++ module, which is compiled
into the engine, an extension built with it is a library of its own, so changing it never means rebuilding Godot. A class starts with GDCLASS(GDExample, Sprite2D), and its static _bind_methods
function, which Godot calls once, registers what scripts and the editor may use:
ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed) for a method and ADD_PROPERTY for a
property backed by a getter and a setter. GDREGISTER_CLASS then makes the class known, and Object::cast_to<T>
converts between classes the way dynamic_cast would.
Not affiliated
Based on public information about the Godot project. MySmartCoPilot is not affiliated with the Godot project, and the Godot project’s real engine internals may differ from what this lesson describes.
Step through one class’s way into each engine, Unreal’s at build time and Godot’s when the extension loads:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Markers in the header. A class derived from UObject carries UCLASS() above it and GENERATED_BODY() inside it, and marks the members the engine should know with UPROPERTY and UFUNCTION.
- The header tool runs first. Before the compiler runs, the Unreal Header Tool, UHT, reads the headers. Epic's documentation calls this step a preprocessing of the headers.
- Generated code. UHT writes the generated header, such as MyObject.generated.h, which the class's header includes last of all its includes. It holds the code that registers the class and its marked members.
- Compiled together. The compiler then builds the class and the generated code together, in the normal build.
- Only marked members. Only marked members make it into the class's description. A member without UPROPERTY is not in it, so the engine cannot see it.
- What the description runs. The UObject system built on that description provides garbage collection, serialization, editor integration, run-time type information and network replication.
- Godot: no header tool. godot-cpp has no tool that reads headers. A class starts with GDCLASS(GDExample, Sprite2D), GDREGISTER_CLASS makes it known, and Godot calls its static _bind_methods() once.
- Methods and properties. Inside it, ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed) registers a method, and ADD_PROPERTY a property backed by a getter and a setter. Scripts and the editor can then use them.
Checkpoint
Check: How Unreal and Godot register classes
A registry you can run shows what a table cannot check
Custom engines often start with a few macros that turn a list of members into a table. This registry is a small, working example:
registry.hpp
// An engine-style property registry built with macros. A class lists its properties once, between REFLECT_BEGIN and
// REFLECT_END, and the macros turn the list into a table: each property's name, kind, flags and where it lives in an
// object. Generic code reads the table to show, edit and save objects of any class it has never seen.
#pragma once
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
enum class Kind { Int, Float, Text };
enum Flags : unsigned { Editable = 1, Saved = 2 };
struct Property {
const char* name;
Kind kind;
unsigned flags;
void* (*address)(void* object); // the member's address inside an object of the class
};
struct ClassInfo {
const char* name;
std::vector<Property> properties;
};
template <typename T>
constexpr Kind kind_of();
template <>
constexpr Kind kind_of<int>() { return Kind::Int; }
template <>
constexpr Kind kind_of<float>() { return Kind::Float; }
template <>
constexpr Kind kind_of<std::string>() { return Kind::Text; }
// REFLECT_BEGIN opens a function that builds the class's table once; each REFLECT_PROPERTY adds one row.
#define REFLECT_BEGIN(Class) \
inline const ClassInfo& class_info_of(const Class*) { \
using Self = Class; \
static const ClassInfo info{#Class, {
#define REFLECT_PROPERTY(member, flags) \
Property{#member, kind_of<decltype(Self::member)>(), flags, \
[](void* object) -> void* { return &static_cast<Self*>(object)->member; }},
#define REFLECT_END() \
}}; \
return info; \
}
template <typename T>
const ClassInfo& class_info() {
return class_info_of(static_cast<const T*>(nullptr));
}
// ---- generic code: it knows the table, not the classes ------------------------------------------------------------
inline std::string text_of(const Property& p, void* object) {
void* at = p.address(object);
switch (p.kind) {
case Kind::Int: return std::to_string(*static_cast<int*>(at));
case Kind::Float: {
char buffer[32];
std::snprintf(buffer, sizeof buffer, "%g", double(*static_cast<float*>(at)));
return buffer;
}
case Kind::Text: return *static_cast<std::string*>(at);
}
return {};
}
inline void set_from_text(const Property& p, void* object, const std::string& text) {
void* at = p.address(object);
switch (p.kind) {
case Kind::Int: *static_cast<int*>(at) = int(std::strtol(text.c_str(), nullptr, 10)); break;
case Kind::Float: *static_cast<float*>(at) = std::strtof(text.c_str(), nullptr); break;
case Kind::Text: *static_cast<std::string*>(at) = text; break;
}
}
// An editor's "details panel": every editable property with its value.
template <typename T>
void details_panel(T& object) {
const ClassInfo& info = class_info<T>();
std::printf("[%s]\n", info.name);
for (const Property& p : info.properties)
if (p.flags & Editable) std::printf(" %-8s = %s\n", p.name, text_of(p, &object).c_str());
}
// Sets a property by its name, as an editor or a console command would. False when the class has no such property.
template <typename T>
bool set(T& object, std::string_view name, const std::string& text) {
for (const Property& p : class_info<T>().properties)
if (name == p.name && (p.flags & Editable)) {
set_from_text(p, &object, text);
return true;
}
return false;
}
// Saves every Saved property as name=value lines, and loads them back.
template <typename T>
std::string save(T& object) {
std::string out;
for (const Property& p : class_info<T>().properties)
if (p.flags & Saved) out += std::string(p.name) + "=" + text_of(p, &object) + "\n";
return out;
}
template <typename T>
void load(T& object, std::string_view text) {
while (!text.empty()) {
const std::string_view line = text.substr(0, text.find('\n'));
text.remove_prefix(std::min(text.size(), line.size() + 1));
const auto eq = line.find('=');
for (const Property& p : class_info<T>().properties)
if ((p.flags & Saved) && line.substr(0, eq) == p.name) set_from_text(p, &object, std::string(line.substr(eq + 1)));
}
} registry_macros.cpp
// Two classes registered with the macros of registry.hpp, then shown, edited and saved by code that never names them.
#include "registry.hpp"
struct Player {
std::string name = "Asha";
int health = 100;
float speed = 4.5f;
int frames_alive = 0; // not in the list below: no panel, no save file and no script will ever see it
};
REFLECT_BEGIN(Player)
REFLECT_PROPERTY(name, Editable | Saved)
REFLECT_PROPERTY(health, Editable | Saved)
REFLECT_PROPERTY(speed, Editable | Saved)
REFLECT_END()
struct Lamp {
float brightness = 0.8f;
int colour = 0xffd27f;
};
REFLECT_BEGIN(Lamp)
REFLECT_PROPERTY(brightness, Editable | Saved)
REFLECT_PROPERTY(colour, Saved)
REFLECT_END()
int main() {
Player player;
Lamp lamp;
details_panel(player);
details_panel(lamp);
set(player, "health", "75");
set(player, "name", "Ravi");
std::printf("set armour: %s\n", set(player, "armour", "5") ? "done" : "Player has no such property");
std::printf("set colour: %s\n", set(lamp, "colour", "0") ? "done" : "colour is not editable");
std::printf("saved Player:\n%s", save(player).c_str());
Player copy;
copy.name = "nobody";
load(copy, save(player));
std::printf("loaded copy: name %s, health %d\n", copy.name.c_str(), copy.health);
} from session import run
# registry_macros.cpp with each compiler; the last line compares the two programs' output.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined registry_macros.cpp -o registry && ./registry
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined registry_macros.cpp -o registry-gcc
./registry > clang.txt && ./registry-gcc > gcc.txt && cmp clang.txt gcc.txt && echo "GCC 16 printed the same"
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined registry_macros.cpp -o registry && ./registry [Player] name = Asha health = 100 speed = 4.5 [Lamp] brightness = 0.8 set armour: Player has no such property set colour: colour is not editable saved Player: name=Ravi health=75 speed=4.5 loaded copy: name Ravi, health 75 $ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined registry_macros.cpp -o registry-gcc $ ./registry > clang.txt && ./registry-gcc > gcc.txt && cmp clang.txt gcc.txt && echo "GCC 16 printed the same" GCC 16 printed the same
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 registry_session.py
Step through what the generic code did with the table, row by row:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Three rows, built once. REFLECT_BEGIN(Player), three REFLECT_PROPERTY lines and REFLECT_END() build Player's table the first time it is asked for. Each row holds a member's name, kind, flags and how to find it in an object.
- A panel from the table. details_panel(player) walks the rows and prints every Editable one, name = Asha, health = 100 and speed = 4.5, without ever naming the Player class.
- Set by name. set(player, "health", "75") finds the row named health and writes 75, and set(player, "name", "Ravi") writes the name. set(player, "armour", "5") finds no row and returns false.
- A row that is not editable. Lamp's colour row has the Saved flag but not Editable, so set(lamp, "colour", "0") is refused too. The flags of a row decide what generic code may do with it.
- Saved and loaded. save(player) writes name=Ravi, health=75 and speed=4.5, one line per Saved row, and load() copied all three into a second player through that text.
- The member no row names. frames_alive is a member of Player, but no row names it, so no panel, save file or script saw it, and nothing warned. The compiler checks each listed member, never the ones left out.
The functions details_panel, set, save and load know only the table, yet they showed both classes, changed a
player’s health by name, refused a property that does not exist and one that is not editable, and copied a player
through its save file. frames_alive never appeared anywhere, because nothing listed it. That is the weakness of
every table kept apart from its class: the compiler checks that each listed member exists and has a type the table
knows, but nothing checks that every member is listed.
Key idea
Generic code sees only what the table lists, so a table kept apart from its class loses a member without a word.
reflection: A program's ability to inspect its own declarations while it is being compiled, for example to list the enumerators of an enumeration with their names. C++26 adds it (P2996R13). More in “C++ enums: enum class and std::to_underlying” removes the list. The compiler can enumerate a class’s members, with
their names, types and annotations such as [[=editable]], while it compiles, so the table follows the class by
itself.
Version note
GCC 16 implements C++26 reflection with -freflection; Clang does not yet.
Checkpoint
Check: A property table kept beside its class
Practise: two ways to build the table
Compare the macro registry above with one that C++26 reflection builds, on four criteria: extra build steps, keeping the table in step with the class, the compilers that build it, and what happens when a member is missing from the table. The questions below take up these criteria and the other ideas above, and start with the one you know least.
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The property table two ways: a forgotten line, then C++26 reflection Pro: opens with a Pro, Premium or Ultimate pass
A table kept apart from its class compiles happily when the two fall out of step. This part reproduces that bug as lost save data, generates the table from markers instead, builds it with C++26 reflection on GCC 16, and compares the three ways on four criteria, with measured compile times.
In this part:
- A forgotten line, reproduced as lost save data
- Generating the table from markers, and where a generator fails
- The same registry with C++26 reflection on GCC 16
- Four criteria, compared
2 runnable examples (Python, drift_macros.cpp, generated_main.cpp, header_tool.py, player_marked.hpp, registry_reflection.cpp) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Garbage collection, RTTI and exceptions the engine way, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Engines change three rules of everyday C++: who frees objects, how a program asks for an object’s type, and how errors travel. This part reproduces the bug a tracing collector punishes, the errors a build without RTTI and exceptions gives on both compilers, and the replacements engines use.
In this part:
- A pointer the collector cannot see, reproduced
- Who frees objects: four models, with their trade-offs
- Builds without RTTI and exceptions, on both compilers
- Cast and error values instead
1 diagram · 2 runnable examples (Python, engine_cast.cpp, gc.cpp, no_rtti.cpp) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is reflection in a game engine, and what does the engine use it for?
Reflection is a description of the program's own classes that code can read: each class's name, its properties with their names, types and flags, and its functions. Standard C++ had none at run time, so engines build it themselves. Unreal Engine generates it from UCLASS, UPROPERTY and UFUNCTION markers with the Unreal Header Tool; godot-cpp registers it in each class's _bind_methods; custom engines keep a table made with macros or a generator. The engine then writes generic code once: the editor's property panel, saving and loading, binding to scripts, network replication and, in Unreal, finding the object pointers the garbage collector must follow. C++26 adds reflection to the language, so the compiler can build such a table from the class itself.
-
Mid to senior
After a release, players say the coins they earn are gone whenever they reload a save. The coins member was added to a class whose properties are registered with a hand-written macro list. Explain the failure, and how you would make it impossible.
-
Mid to senior
In an engine with a tracing garbage collector, a character's spare weapon sometimes reports nonsense damage, and now and then the game crashes. The spare is held by a plain pointer member of the character. Diagnose the bug and fix it.
-
Senior to staff
Your engine is built without exceptions and RTTI. A teammate wants both switched on so that a new library can throw and use dynamic_cast. What do you check, what would it cost, and what do engines use instead?
Key takeaways
- Engines add reflection to C++: a description of each class that drives the editor, saving, scripting, networking and, in Unreal, garbage collection.
- C++ in Unreal marks classes and members with
UCLASS,UPROPERTYandUFUNCTION, and the Unreal Header Tool generates the registration code before compiling. - Godot’s godot-cpp registers classes with
GDCLASSand_bind_methods, as an extension library that never needs an engine rebuild. - A table kept apart from its class can fall out of step without any error; C++26 reflection, in GCC 16 with
-freflection, builds it from the class itself.
References
- Objects in Unreal Engine (Epic Games)
- Reflection System in Unreal Engine (Epic Games)
- Epic C++ Coding Standard for Unreal Engine (Epic Games)
- Module Properties in Unreal Engine (Epic Games)
- About godot-cpp (Godot Engine documentation)
- Getting started: creating a simple plugin (godot-cpp) (Godot Engine documentation)
- Object class (Godot Engine documentation)
- P2996R13: Reflection for C++26 (ISO/IEC JTC1/SC22/WG21)
- P3394R4: Annotations for Reflection (ISO/IEC JTC1/SC22/WG21)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress