| name | ue-cpp-style |
| description | Use when writing or reviewing Unreal Engine C++ code. Authoritative style guide covering naming, formatting, const, ownership, pointers, lambdas, UE vs std choices, reflection macros, and modern C++ usage. Domain knowledge independent of review process. |
| axis | domain |
Unreal Engine C++ Style Guide
These rules merge Epic's official coding standard with modern best practices. When they conflict, the modern practice
wins — it produces cleaner, safer code.
File Header
Every source file (.h, .cpp) must begin with:
Naming
- PascalCase for all types, functions, member variables, namespaces, enums, enum values.
- Type prefixes — enforced by UHT for reflected types:
U — UObject subclasses (UActorComponent)
A — AActor subclasses (APlayerController)
S — SWidget subclasses (SCompoundWidget)
F — structs and non-UObject classes (FVector, FMyStruct)
T — templates (TArray, TSubclassOf)
I — abstract interfaces (IInteractable)
E — enums (EMovementMode)
C — concept-alike structs (CStaticClassProvider)
b — booleans (bIsVisible, bHasStarted)
- No prefix on file names —
BuildableActor.cpp, not ABuildableActor.cpp.
Out prefix on non-const reference parameters the function writes to.
In prefix to disambiguate template parameters from nested aliases.
- Method names are verbs:
GetHealth(), IsAlive(), ApplyDamage().
- Bool-returning functions ask a question:
IsVisible(), ShouldUpdate(), CanInteract().
- Procedures use strong verb + object:
DestroyWidget(), SpawnGuest().
- Macros:
UE_ prefix, SCREAMING_SNAKE_CASE (UE_LOG, UE_AUDIT_IMPORT).
using over typedef — supports templates and is clearer for function pointers.
Indentation and Formatting
A .clang-format file at the repository root is the authoritative source. Run clang-format -i <file> to auto-fix.
- Tabs for all indentation. No spaces.
- Allman brace style — opening
{ on a new line, at the same indentation as the scope identifier.
- All scopes indent — namespaces, classes, functions, control flow, lambdas.
- Access modifiers (
public:, private:, protected:) are flush with the class/struct keyword.
namespace Resort
{
class FMyClass
{
public:
void DoWork()
{
if (bReady)
{
Execute();
}
}
};
}
General Style
Production Code Is for Production Concerns
A production code file (a .h or .cpp outside /Private/Tests/, *.spec.cpp, or other test-only directories) must
read as a description of what the system does, not how it is exercised. Someone reading a production code file must
not be able to discern anything about how tests are conducted. That is noise. Reading the production code must only
involve understanding production concerns.
Implementers — write production code that is inherently testable. Tests reach the behaviour through the same public
surface real callers use. Do not add arbitrary test-only functions, accessors, or callbacks to satisfy a simplistic
or invasive test design. If a test cannot reach a behaviour through the production surface, the test design is
wrong, not the production code — redesign the test.
Reviewers — BLOCKING. Any of the following in a production code file is BLOCKING:
- A function, member, or macro whose name contains
ForTesting, _ForTest, TestSeam, TestOnly, TestHook, or any
equivalent.
- A function whose only documented purpose is to be called from tests (Doxygen comments that begin "Test seam:", "Used
by tests to…", "Tier N specs use this to…", etc.).
- A
friend declaration against a test fixture whose only purpose is to expose private state to tests.
- A public accessor whose comment explains the test scenario that needs it rather than the production caller
that needs it.
- Any occurrence of
WITH_DEV_AUTOMATION_TESTS in a file outside /Private/Tests/. This macro gates test
infrastructure; it must never appear in production code, full stop. A file outside /Private/Tests/ that contains
this macro is suspect as a whole — investigate the entire file, not just the gated block, since the seam is rarely
isolated.
Cite the exact symbol or comment as evidence. The fix is always to remove the seam from production and rewrite the test
against the production surface.
What Is Not a Test Seam
The following gates run code in production modes of the engine, not test modes, and are not covered by this
rule:
#if WITH_EDITOR — gates code for the editor build configuration. Editor-gated members, callbacks, and overrides
(PostEditChangeProperty, CanEditChange, editor-only validation) are legitimate production code.
#if WITH_EDITORONLY_DATA — gates editor-only data fields. Same rationale: the editor is a production mode of the
engine.
Commentary Density
Signal thresholds for production files (comment percentage of total file lines):
| Range | .h | .cpp |
|---|
| < 5% | Good — self-explanatory design | Good |
| 5–10% | Acceptable — stating intent and API rationale | Acceptable for difficult bridging/adaptor designs |
| 10–25% | Acceptable for difficult bridging/adaptor designs | Orange flag |
| > 25% | Orange flag | Red flag |
| > 40% | BLOCKING in any file | BLOCKING in any file |
Heuristics for every comment:
- Can you form a question beginning with "why" that this comment answers? If not, the comment is probably unnecessary.
- Does the answer to that question point directly at a behavioural specification from a design document? If not,
examine the relevance of the question and comment. Is the comment warning about a safety invariant that should
actually be structurally enforced?
A rising comment ratio is always a signal of decreasing code quality — not a mitigation for it. Comments are not a
substitute for re-evaluating assumptions and performing a proper course correction.
auto Usage
When the type already appears on the line (Cast<>, constructors, MakeUnique<>, etc.), prefer auto over repeating the
type.
- Use
auto* after Cast<> — type is in the template argument. Never omit the explicit *.
auto* const Comp = Cast<UStaticMeshComponent>(Component);
- Use
auto& when the type is obvious from context (e.g. auto& LAM = GetWorld()->GetLatentActionManager();).
- Range-for:
auto const& is the default. The container on the right of : makes the element type obvious;
explicit element types in range-for are never preferred. Use auto& only when mutating; use auto (by value) only
for trivially-copyable elements when a copy is deliberate.
for (auto const& Str : Names) { }
for (auto& Slot : Inventory) { }
for (auto const& [K, V] : Map) { }
for (FString const& Str : Names)
for (auto X : Names)
- Avoid bare
auto when the type isn't visible on the same line (range-for is the established exception above — the
container makes the element type obvious).
- East-const with auto —
const auto X = Cast<>() makes X a raw pointer, not const pointee. Use east-const:
auto* const (const ptr), auto const* (ptr to const), auto const& (const ref).
- Structured bindings (
auto [X, Y] = Func();) are encouraged in non-reflected code. Use Tie(X, Y) for
reassignment.
const — Where It Matters
- Do:
const on pointers/references to const objects, on member functions, on range-for when not mutating:
for (auto const& Str : Array) (see auto Usage for the full range-for rule).
- Do:
const on return-by-reference: TArray<FString> const& GetNames() const;
- Don't:
const on return-by-value — it inhibits move semantics.
- Nuanced:
const on local values and by-value params is optional. Consistency within a file matters most.
- East-const preferred: place
const on the right. T* const (const pointer), T const* (pointer to const),
T const& (const reference).
Ownership and Pointers
Decision Tree
Is it a UObject?
├─ Member field? → TObjectPtr<UMyClass> (always — no exceptions)
├─ Function param, null is valid? → UMyClass* (raw pointer)
├─ Function param, null is a bug? → UMyClass& (reference)
├─ Cached ref that might get GC'd? → TWeakObjectPtr<UMyClass>
└─ Asset reference (lazy load)? → TSoftObjectPtr<UMyClass> / FSoftObjectPath
Is it a non-UObject on the heap?
├─ Single owner? → TUniquePtr<FMyStruct> via MakeUnique<>()
├─ Shared ownership? → TSharedPtr<FMyStruct> via MakeShared<>()
├─ Non-owning observer of shared? → TWeakPtr<FMyStruct>
└─ Stack/member by value? → Use the value directly
TObjectPtr — Member Fields
All member pointers to UObject types use TObjectPtr<>. No exceptions:
UPROPERTY()
TObjectPtr<UStaticMeshComponent> MeshComponent;
TSharedRef — Non-Nullable, Must Not Be Moved
TSharedRef must not be moved — MoveTemp(TSharedRef) compiles but triggers an ensure at runtime. Pass as
TSharedRef<T> const& (read) or TSharedRef<T>& (re-seat). Never pass by value.
Avoid as a container value type — any operation that default-constructs or moves elements ensures. Safe only when every
insertion supplies a pre-constructed ref and no default-construction path exists.
TSharedRef<FMyData> Moved = MoveTemp(Original);
TSharedRef<FMyData> Copy = Original;
TMap<FName, TSharedRef<FMyData>> Map;
Map.FindOrAdd(Key) = MakeShared<FMyData>();
TMap<FName, TSharedPtr<FMyData>> Map;
Never TSharedPtr<UObject>
UObjects are GC-managed, not shared-pointer-managed. Never wrap UObjects in TSharedPtr.
Lambda Captures and Pointer Safety
Deferred lambdas (delegates, timers, async) must use CreateWeakLambda / CreateSPLambda, or capture TWeakObjectPtr
and pin at invocation:
AActor* MyActor = GetOwner();
GetWorld()->GetTimerManager().SetTimer(Handle, [MyActor]()
{
MyActor->DoThing();
}, 1.0f, false);
TWeakObjectPtr<AActor> WeakActor = GetOwner();
GetWorld()->GetTimerManager().SetTimer(Handle, [WeakActor]()
{
if (auto Pinned = WeakActor.Pin())
{
Pinned->DoThing();
}
}, 1.0f, false);
Anti-patterns
.Get() on a smart pointer — maximum-severity red flag. Bypasses RAII. The only legitimate use is lending a raw
pointer to an API outside C++. See the dedicated section below.
- Raw
UObject* member fields — always TObjectPtr<>
- Raw
new/delete for non-UObjects — always MakeUnique/MakeShared
- Storing
TObjectPtr<> without UPROPERTY() — GC can't see it
- Storing
TObjectPtr<> AT ALL in a non-reflected (non-USTRUCT or non-UCLASS) type — GC can't see it
TSharedPtr<UMyObject> — never wrap UObjects in shared pointers
TWeakObjectPtr for guaranteed-lifetime refs — unnecessary overhead
.Get() on a Smart Pointer Is a Maximum-Severity Red Flag
Calling .Get() on a smart pointer is a maximum-severity red flag -- TUniquePtr,
TSharedPtr, TSharedRef, TWeakPtr, and TWeakObjectPtr alike. These types exist to
encode ownership and lifetime through RAII. .Get() discards that contract and yields a
raw pointer with no lifetime guarantee. Any code downstream of a .Get() site has no way
to know the pointee is still alive.
The only legitimate reason to call .Get() is to lend the raw pointer to an API outside
C++ -- a C FFI boundary, SQLite, or another third-party library that cannot accept a C++
smart pointer type. Good C++ has almost no other reason to bypass a smart pointer's RAII
contract via .Get(). When you see one in a review, treat it as a defect until proven
otherwise.
What to do instead
Immediate consumption (callee uses the object within the call, then returns):
dereference the smart pointer at the call site.
DoWork(Shared.Get());
DoWork(*Shared);
Shared->DoWork();
Stored consumption (callee keeps the pointer beyond the call): pass the smart
pointer itself so ownership flows through the type system.
void Register(FMyData* Raw);
Register(Shared.Get());
void Register(TSharedPtr<FMyData> Data);
Register(Shared);
void Register(TWeakPtr<FMyData> Weak);
Register(Shared);
Weak references (TWeakPtr, TWeakObjectPtr): .Pin() promotes the weak ref to a
strong handle for the duration of use -- TWeakPtr::Pin() returns a TSharedPtr,
TWeakObjectPtr::Pin() returns a TStrongObjectPtr. Both preserve RAII. .Get() on a
weak pointer is the same anti-pattern as on a shared pointer -- it hands out a raw pointer
and throws away the guarantee you just went looking for.
if (UMySubsystem* Raw = Weak.Get())
{
Raw->DoWork();
}
if (auto Pinned = Weak.Pin())
{
Pinned->DoWork();
}
Deferred callables (lambdas, delegates, task queues, coroutine state): capture a weak
pointer and pin at invocation. .Get() in a capture is silent UB waiting for a reorder.
MyDelegate.BindLambda([Raw = Shared.Get()]()
{
Raw->DoWork();
});
MyDelegate.BindLambda([Weak = TWeakPtr<FMyData>(Shared)]()
{
if (auto Pinned = Weak.Pin())
{
Pinned->DoWork();
}
});
Review posture
Existing code contains .Get() call sites -- particularly on TWeakObjectPtr -- that
survived earlier, laxer guidance. Same-scope synchronous uses are not correctness bugs and
do not require a crash-stop rewrite. New code must not add them; refactors that touch such
a call site should replace .Get() with the RAII-preserving alternative on the way past.
Lambdas
- Explicit captures — never
[&] or [=]. Prefer [this] over [=].
- Deferred lambdas: use
CreateWeakLambda / CreateSPLambda, or capture TWeakObjectPtr and validate.
- Keep lambdas short — a couple statements max when inline in an expression.
- Explicit return types on large lambdas.
- AVOID Immediately Invoked Lambda Expressions (IILE). They are an anti-pattern and suggest the overall design needs
another look. A last resort only when all alternatives are less readable.
Namespaces
- Namespaces are supported for non-reflected code (free functions, utilities, constants).
- Cannot wrap
UCLASS, USTRUCT, UENUM — UHT does not support it.
- Use
Private sub-namespace for implementation details: UE::Audio::Private::.
- Macros cannot live in namespaces — use
UE_ prefix instead.
- Never use anonymous namespaces (
namespace { ... }) — UE unity builds merge multiple .cpp files into one
translation unit, causing symbol collisions between identically-named functions in different files. Use a named
namespace for file-local helpers instead.
- Never use
using namespace in .cpp files — unity builds amalgamate TUs, causing name collisions across files.
Standard Library vs UE
Epic's guidance: "prefer the option which gives superior results."
Containers
TArray, TMap, TSet — required for UPROPERTY. Note: UE containers use FMemory::Realloc on their backing
buffer, bypassing constructors and destructors on existing elements. Non-trivially-copyable types (e.g.
std::function, std::atomic) are unsafe in UE containers.
std::vector — acceptable for internal non-reflected code when you need correct handling of
immovable/non-copyable types.
- Never call
.begin()/.end() directly on UE containers — they exist only for implicit range-for. Bridge to std
algorithms via GetData()/GetNum() or a std::span adapter.
Callables — TFunction, Never std::function
std::function — NEVER. TArray growth uses FMemory::Realloc on the backing buffer, bypassing all constructors
and destructors. MSVC's std::function SBO implementation stores self-referential pointers that become stale after
realloc, causing use-after-free crashes on invocation. This applies even to std::function stored as a member of a
struct that ends up in a UE container.
Use TFunction for stored callables and TFunctionRef for non-owning parameter passing. std::function has no
safe use in this codebase.
Atomics — std::atomic Has Restrictions
std::atomic has deleted copy/move constructors. FMemory::Realloc bypasses these during growth (technically UB), and
within-buffer operations like RemoveAt/Insert will fail to compile. std::atomic remains correct for standalone
fields in native C++ classes where the variable's address never changes. For per-element counts in arrays, use plain
int32 and serialize mutations via TMpscQueue.
Algorithms
Prefer std::sort and <algorithm> — they outperform UE equivalents. Bridge via std::span adapter:
auto AsSpan(auto& Container)
{
return std::span(GetData(Container), GetNum(Container));
}
std::sort(AsSpan(MyArray).begin(), AsSpan(MyArray).end(), Predicate);
If bridging feels heavyweight, TArray::Sort() with a predicate is acceptable.
Quick Reference Table
Use std:: | Use UE |
|---|
std::atomic (standalone fields only) | TArray, TMap, TSet (for UPROPERTY) |
std::sort, <algorithm> | TFunction / TFunctionRef (not std::function) |
std::numeric_limits | FString, FName, FText |
std::unique_ptr (= TUniquePtr) | TSharedPtr (UE ecosystem compat) |
std::tuple (TVariant/TTuple incomplete) | TOptional, TDelegate, MoveTemp |
std::string at interop boundaries only | TObjectPtr, TWeakObjectPtr, TSoftObjectPtr |
UE5Coro Aggregate Awaiter Hazard
WhenAll, WhenAny, and Race must be constructed before the work they await can complete. Construct on the game
thread before dispatching signals or tasks into the Mass/task system. If an input coroutine completes before the
aggregate is wired up, a self-deadlock occurs via non-recursive mutex re-acquisition.
TArray<UE5Coro::TCoroutine<>> Awaiters;
for (auto& Launcher : Launchers) { Awaiters.Add(Launcher(Dispatcher)); }
auto Completion = UE5Coro::WhenAll(Awaiters);
co_await UE5Coro::Async::MoveToTask();
co_await Completion;
co_await UE5Coro::Async::MoveToTask();
co_await UE5Coro::WhenAll(Awaiters);
Modern C++ Features
UE compiles with C++20. Encouraged in non-reflected code.
if constexpr — compile-time branching in templates. Eliminates SFINAE in many cases.
- Concepts and
requires — prefer over TEnableIf/SFINAE.
[[nodiscard]] — on functions where ignoring the return is almost certainly a bug.
- Designated initializers — fine for POD/aggregate initialization in non-reflected code.
- Three-way comparison (
auto operator<=>(…) = default) — available but rarely needed.
- Ranges — available via
std::span adapter over UE containers.
consteval — forces compile-time evaluation.
constinit — ensures static initialization at compile time without making the variable const.
- Float literals in generic code — use
0 and 1 instead of 0.0f and 1.0f to avoid overload ambiguity with LWC
double APIs.
- Integer types —
int is safe on all UE platforms (guaranteed >= 32 bits). Reserve int32 for UPROPERTY and
serialized values to signal explicit width intent.
UE Reflection (UCLASS / USTRUCT / UPROPERTY / UFUNCTION)
Class Layout
UCLASS()
class MYMODULE_API AMyActor : public AActor
{
GENERATED_BODY()
public:
AMyActor();
protected:
virtual void BeginPlay() override;
private:
UPROPERTY()
TObjectPtr<USceneComponent> Root;
};
GENERATED_BODY() only — never GENERATED_UCLASS_BODY() or GENERATED_USTRUCT_BODY().
- Access specifiers go AFTER
GENERATED_BODY().
- Public interface first, then protected, then private.
- UHT does not support namespaces around reflected types — reflected classes must be at global scope.
UPROPERTY Exposure Specifiers — Use the Narrowest Scope
| Specifier | When to use |
|---|
EditAnywhere | Designer needs to tune per-instance and per-class |
EditDefaultsOnly | Should be consistent across all instances |
EditInstanceOnly | Varies per placement but shouldn't change the archetype |
VisibleAnywhere | Computed/code-managed value the designer should see |
All UPROPERTY pointers to UObject types must use TObjectPtr<>:
UPROPERTY()
TObjectPtr<UStaticMeshComponent> MeshComp;
UPROPERTY()
UStaticMeshComponent* MeshComp;
Boolean properties: use bool bFlag by default. Use uint8 bFlag : 1 bitfields when you have many boolean flags and
memory layout matters (replicated structs, Mass fragments).
UFUNCTION Specifiers
| Specifier | When to use |
|---|
BlueprintCallable | Can be called from Blueprint |
BlueprintPure | No side effects, no exec pin — use for getters |
BlueprintImplementableEvent | Declared in C++, implemented in Blueprint |
BlueprintNativeEvent | C++ default, overridable in Blueprint |
CallInEditor | Callable from details panel button in editor |
USTRUCT
USTRUCT(BlueprintType)
struct FMyData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
FString Name;
UPROPERTY(EditAnywhere)
int32 Count = 0;
};
Default member initializers in the struct body. No constructors needed for simple data structs — use designated
initializers.
UENUM
UENUM(BlueprintType)
enum class EGuestState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Walking UMETA(DisplayName = "Walking"),
Skiing UMETA(DisplayName = "Skiing"),
};
Blueprint-exposed enums must be : uint8. Use UMETA(DisplayName = "…") for editor-friendly names.
Slate Styling
- No magic numbers in Slate code. Every padding, margin, width, height, and gap must reference a named token or
constant.
- Use
StyleKeys::PaddingAmount(EUiSize) for padding and margins. Scale relative to 20px base: XSmall (12.8), Small (
16), Medium (20), Large (24), XLarge (30), XXLarge (40).
- Use named constants in
StyleKeys for fixed dimensions. Current constants: ButtonSlotGap (4), NarrowDialogWidth (
400), StandardDialogWidth (800).
- If no token exists for a value, create one in
StyleThemeKeys.h — never use a literal.
- Use
ComposeDefaultTableSetup() from TableSetupHelpers.h for standard table style composition. Override only what
differs via FTableSetupParams designated initializers.