UObject and Reflection — what UHT generates and why it changes everything
UObject in Unreal is not an ordinary C++ class. It's the entry point into the engine's reflection system. When you write UCLASS(), UPROPERTY(), UFUNCTION(), these macros are not just hints. They are instructions for Unreal Header Tool (UHT) — a separate program that runs before the C++ compiler, parses your headers, and generates code: property tables, factories, Blueprint wrappers, GC markers, serialization helpers. That generated code is what turns a plain class into a full citizen of the engine.
This is why UObject classes get everything considered "Unreal magic": automatic garbage collection (the GC scans only UPROPERTY fields), serialization of assets and save games without manual boilerplate, a bridge to the Blueprint VM, network replication, the editor Details Panel — all of it works through the same reflection table UHT assembles from your macros. Classes outside the UObject hierarchy get none of this — even if held inside TSharedPtr, they remain invisible to the engine.
Three macros — UCLASS, UPROPERTY, UFUNCTION — are three doors into that system. Each opens its own set of capabilities: UCLASS registers the type and creates a CDO (Class Default Object — the template every instance is copied from); UPROPERTY adds a field to the property table (enabling GC tracking, serialization, BP exposure, replication); UFUNCTION does the same for methods (BP calls, RPCs, exec console, delegates). Specifiers inside the macros are the second control layer: they say what exactly to enable (editor only? Blueprint too? writable? replicated?). The full map is in the layers below.
Topic map
- UObject as base class — root of UE's object model, how it differs from plain C++ classes, what inheritance buys you.
- UCLASS macro — what it registers, what metadata it generates, common class specifiers.
- GENERATED_BODY — the placeholder UHT expands into real boilerplate: type info, constructors, accessors, RTTI helpers.
- UHT and compilation — the two-stage pipeline: UHT parses headers, generates
.generated.hand.gen.cpp, then the C++ compiler builds everything together. - Reflection system —
UClass,FProperty,UFunction, runtime field and method iteration, metadata. - UPROPERTY macro — three roles: GC tracking, Blueprint exposure, editor visibility. Without it, the field is invisible to the engine.
- UFUNCTION macro —
BlueprintCallable, RPCs,Exec,Server/Client/NetMulticast, delegates. - Property specifiers — two axes:
Edit*/Visible*(editor) andBlueprintReadWrite/BlueprintReadOnly(BP), plusReplicated,Transient,SaveGame. - Blueprint exposure — what you need to combine to make a field/method visible in BP: macro + specifier + correct type.
- Blueprint architecture — a Blueprint is a
UClassextending a native one, with its own bytecode executed by the BP VM. - Class Default Object —
CDO, the per-class defaults template, instance ↔ CDO relationship, delta serialization. - Object lifecycle — Constructor →
PostInitProperties→PreInitializeComponents→BeginPlay→Tick→BeginDestroy. - Garbage collection — why
UPROPERTY= GC-tracked, how the reachability graph is scanned, what happens without the macro. - Pointer / reference types — raw
UPROPERTY*,TObjectPtr,TWeakObjectPtr,TSoftObjectPtr,TSubclassOf— what each means for the GC and for loading. - Soft references —
TSoftObjectPtr/TSoftClassPtr, async loading,FStreamableManager, asset streaming.
Common traps
| Mistake | Consequence |
|---|---|
Forgetting UPROPERTY() on a UObject* | GC can't see the reference; the target is collected, the pointer dangles, crash later |
Holding UObject via std::shared_ptr | RAII wrappers don't integrate with GC; the reference isn't counted, the object gets collected |
| Gameplay logic in the constructor | The constructor also runs on the CDO — no world, no other actors; crash or undefined behaviour |
Declaring UCLASS() but forgetting GENERATED_BODY() | Link errors: UHT-generated functions are missing |
A field with no UPROPERTY() but BlueprintReadWrite | UHT compile error |
| Treating a BP default the same as the CDO | A BP instance stores a delta against the CDO; touched properties ignore the CDO |
Renaming a UPROPERTY without CoreRedirects | Saved assets lose the field's value — silent data loss |
| Expecting Live Coding to refresh UHT tables | LC patches machine code but not reflection; needs an editor restart |
Treating BlueprintImplementableEvent as a synonym for BlueprintNativeEvent | The first forbids a C++ body; the second requires _Implementation |
Storing a UObject* without UPROPERTY inside TSharedPtr | Same as the first row: GC invisible, dangling pointer |
Using dynamic_cast instead of Cast<> | C++ RTTI works, but Cast<> is UClass-aware; the only canonical path |
TSoftObjectPtr without LoadSynchronous or async loading | You never get the object; Get() returns nullptr until loaded |
Interview relevance
UObject and reflection are a mandatory senior-level topic for any UE5 interview. It's the foundation everything else stands on. Expect to be checked on:
- That
UObjectreflection is a separate system, not C++ RTTI. RTTI gives only type identity anddynamic_cast; reflection enumerates fields and methods by name. - That UHT is a separate program running before the C++ compiler and producing
.generated.h. If UHT didn't run, the build breaks. - That
UPROPERTYis not just an editor hint: it's the entry into GC, serialization, replication, Blueprint, networking. - That CDO is the template instance holding a class's defaults. An instance is a delta against the CDO, not a copy.
- That a UObject's lifecycle is not the same as a C++ constructor: there's
PostInitProperties,BeginPlay,BeginDestroy. Logic that needs a world must live inBeginPlay. - That
TObjectPtrvsTWeakObjectPtrvsTSoftObjectPtrare different GC semantics, not different syntax. One keeps the object alive, one doesn't and nulls on collection, the third is an asynchronous path.
Common wrong answer: "UObject is just a base class, like Object in Java or Python." The real answer: UObject is the entry point into reflection, without which no standard engine subsystem works. And most importantly — Unreal's reflection is generated by UHT, not deduced by the compiler; that's why header changes require an editor restart and not just Live Coding.