~/blog $ cat ue5-cpp-vs-standard-cpp.md
Unreal Engine 5 C++ vs. Modern Standard C++
Unreal Engine 5 (UE5) is one of the most powerful and popular game engines in the industry, renowned for its cutting-edge graphics, robust toolsets, and versatility in creating everything from AAA games to VR simulations and architectural visualizations. At the core of Unreal Engine 5 is C++, a language chosen for its performance and flexibility.
However, Unreal Engine’s implementation of C++ differs significantly from modern standard C++ (C++11, C++14, C++17, and C++20). These differences can be daunting for programmers new to Unreal Engine development, and understanding them is crucial for using the engine’s capabilities efficiently and integrating seamlessly with its ecosystem.
In this guide, we’ll explore the unique features and variations of Unreal C++ compared to modern standard C++. Whether you’re a seasoned C++ developer or new to the language, this guide will help you navigate the specific requirements and conventions of Unreal Engine 5 C++ programming for a smoother transition and a more effective development process.
Key Differences: Unreal C++ vs. Standard C++
Understanding the key differences between Unreal Engine C++ and modern standard C++ is essential for any programmer looking to develop with Unreal Engine 5. Let’s dig into the unique aspects of Unreal C++ and how they compare to standard C++.
Reflection System
One of the most significant differences in Unreal C++ is the reflection system. Standard C++ has no built-in reflection; Unreal Engine adds it through macros that attach metadata to your types. The Unreal Header Tool (UHT) parses these macros and generates the code that lets the engine inspect and manipulate your types at runtime — which is critical for features like Blueprints, garbage collection, and serialization.
UCLASS: Declares a class that is reflected and can be instantiated by the engine. Classes with this macro are eligible for Blueprint integration and other engine features.
UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Class properties and methods
};
USTRUCT: Similar to UCLASS, but for structs.
USTRUCT(BlueprintType)
struct FMyStruct
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyValue;
};
UENUM: Makes an enum usable in Blueprints and other engine systems.
UENUM(BlueprintType)
enum class EMyEnum : uint8
{
Value1 UMETA(DisplayName = "Value 1"),
Value2 UMETA(DisplayName = "Value 2")
};
UPROPERTY: Marks a class member for reflection and garbage-collection tracking, and (depending on the specifiers you pass) makes it visible and editable in the Unreal Editor.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MyProperty;
UFUNCTION: Marks a function for reflection, allowing it to be called from Blueprints or other engine systems.
UFUNCTION(BlueprintCallable)
void MyFunction();
Garbage Collection
Unreal Engine has its own garbage collection system that manages memory for UObject-derived classes. The collector periodically scans for objects that are no longer referenced and destroys them, which helps prevent memory leaks and ensures proper cleanup.
UPROPERTY keeps objects alive: For the garbage collector to see a reference, the pointer must be marked with UPROPERTY. An unmarked raw pointer to a UObject is invisible to the GC — the object can be collected out from under you.
UPROPERTY()
UObject* MyObject;
// UE5 also offers TObjectPtr, which behaves like a raw pointer
// in packaged builds but adds access tracking in the editor:
UPROPERTY()
TObjectPtr<UObject> MyOtherObject;
Manual memory management: In standard C++, developers use new and delete for dynamic memory, along with smart pointers (std::shared_ptr, std::unique_ptr). In Unreal, this is unnecessary for UObjects — you create them with NewObject (or SpawnActor for actors) and the engine handles their lifecycle. Never call delete on a UObject.
Object Lifecycle
Unreal Engine introduces a distinct object model centered around UObject, the base class for all objects managed by the engine.
UObject system: All objects that require reflection, serialization, or garbage collection must derive from UObject.
UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY()
};
Actor lifecycle: Actors are special UObjects that exist within the game world and have additional lifecycle methods:
- BeginPlay: Called when the actor enters the world (spawned or level start).
- Tick: Called every frame while the actor has ticking enabled.
- EndPlay: Called when the actor is about to be removed or destroyed.
UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};
These key differences illustrate how Unreal Engine C++ extends and adapts standard C++ to meet the unique needs of game development.
Unique Features in Unreal C++
Unreal Engine 5 introduces several unique features and systems that have no counterpart in standard C++. They’re designed specifically to streamline the creation of complex, interactive experiences.
Blueprint Integration
Unreal C++ is designed to integrate seamlessly with Blueprints, Unreal Engine’s visual scripting language. This integration lets you expose C++ functionality to designers and artists who prefer a visual approach.
BlueprintCallable: Marks a function as callable from Blueprints.
UFUNCTION(BlueprintCallable)
void MyBlueprintFunction();
BlueprintImplementableEvent: Declares a function in C++ whose implementation lives entirely in Blueprints.
UFUNCTION(BlueprintImplementableEvent)
void MyBlueprintEvent();
BlueprintNativeEvent: Provides a base C++ implementation while allowing Blueprint overrides. The C++ body goes in the generated _Implementation function.
UFUNCTION(BlueprintNativeEvent)
void MyNativeEvent();
virtual void MyNativeEvent_Implementation();
Modules and Plugins
Unreal Engine’s modular architecture organizes code into modules and plugins, which differs from typical standard C++ project structures.
Modules are logical units of code that can be independently compiled and linked. Each module has a .Build.cs file (written in C#) that defines its dependencies and build rules:
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });
}
}
Plugins are packages of reusable functionality that can be shared across projects. A plugin contains one or more modules and can be enabled or disabled in the Unreal Editor. Its .uplugin file defines the plugin’s metadata and modules:
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "My Plugin",
"Description": "A description of my plugin.",
"Category": "Gameplay",
"Modules": [
{
"Name": "MyModule",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
}
Actor System
Unreal’s Actor system is a fundamental part of game development, providing a framework for entities within the game world.
AActor: The base class for all actors in the game world.
UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
GENERATED_BODY()
};
Components: Building blocks for actors, providing modular functionality:
- USceneComponent: Base class for components that have a transform and can be attached in a hierarchy.
- UPrimitiveComponent: Base class for components that can be rendered or have collision.
UPROPERTY(VisibleAnywhere)
USceneComponent* Root;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* Mesh;
Network Programming
Unreal Engine simplifies network programming with built-in support for replication and remote procedure calls (RPCs) — something standard C++ leaves entirely to external libraries.
Replication automatically synchronizes object state across the network. Mark the property with Replicated and register it in GetLifetimeReplicatedProps:
UPROPERTY(Replicated)
int32 MyReplicatedProperty;
// In the .cpp file:
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyActor, MyReplicatedProperty);
}
RPCs are functions executed on a remote machine. The body of an RPC goes in the generated _Implementation function:
UFUNCTION(Server, Reliable)
void ServerFunction();
// In the .cpp file:
void AMyActor::ServerFunction_Implementation()
{
// Runs on the server
}
Gameplay Framework
Unreal Engine provides a comprehensive gameplay framework — a structured set of predefined classes for building a game:
GameMode defines the rules and flow of the game:
UCLASS()
class MYPROJECT_API AMyGameMode : public AGameModeBase
{
GENERATED_BODY()
};
PlayerController manages player input and controls:
UCLASS()
class MYPROJECT_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
};
GameState holds global game state information:
UCLASS()
class MYPROJECT_API AMyGameState : public AGameStateBase
{
GENERATED_BODY()
};
PlayerState holds per-player state information:
UCLASS()
class MYPROJECT_API AMyPlayerState : public APlayerState
{
GENERATED_BODY()
};
Unreal’s Versions of Standard Library Features
Many standard C++ features and libraries have Unreal-specific counterparts tailored to the engine’s needs. Understanding these differences is crucial for writing efficient and maintainable Unreal C++ code.
Containers
Unreal Engine provides its own container classes, optimized for game workloads and integrated with the engine’s systems (reflection, serialization, memory allocators).
TArray: Dynamic array, the equivalent of std::vector.
TArray<int32> MyArray;
MyArray.Add(10);
MyArray.RemoveAt(0);
TMap: Associative key-value container. Note that TMap is hash-based, so it’s closer to std::unordered_map than to the ordered std::map.
TMap<FString, int32> MyMap;
MyMap.Add(TEXT("Key"), 100);
int32 Value = MyMap[TEXT("Key")];
TSet: Hash-based collection of unique elements, closer to std::unordered_set than to the ordered std::set.
TSet<int32> MySet;
MySet.Add(42);
MySet.Remove(42);
Smart Pointers
Unreal Engine ships its own smart pointer library for managing the lifetime of regular (non-UObject) C++ objects. An important point that trips up many newcomers: these smart pointers are separate from the garbage collector and must not be used to hold UObjects. UObjects are managed by the GC via UPROPERTY references; for a non-owning reference to a UObject, use TWeakObjectPtr instead.
TSharedPtr: Reference-counted shared ownership, similar to std::shared_ptr.
TSharedPtr<FMyClass> MySharedPtr = MakeShared<FMyClass>();
TWeakPtr: Non-owning weak reference to an object managed by a TSharedPtr, similar to std::weak_ptr.
TWeakPtr<FMyClass> MyWeakPtr = MySharedPtr;
TUniquePtr: Exclusive ownership, similar to std::unique_ptr.
TUniquePtr<FMyClass> MyUniquePtr = MakeUnique<FMyClass>();
String Handling
Instead of std::string, Unreal Engine provides three string classes, each with a distinct purpose:
FString: Mutable, dynamic string for general manipulation.
FString MyString = TEXT("Hello, Unreal!");
MyString.Append(TEXT(" How are you?"));
FName: Immutable, case-insensitive identifier stored in a global name table, making comparisons extremely cheap. Ideal for object and asset names.
FName MyName(TEXT("Player"));
FText: Localizable text for anything displayed to the user.
FText MyText = FText::FromString(TEXT("Welcome to Unreal Engine"));
Math Library
Unreal Engine provides its own math classes optimized for 3D graphics and game development.
FVector: A 3D vector.
FVector MyVector(1.0f, 2.0f, 3.0f);
MyVector.Normalize();
FRotator: Rotation expressed as pitch, yaw, and roll.
FRotator MyRotator(45.0f, 90.0f, 0.0f);
FTransform: Combines translation, rotation, and scale.
FTransform MyTransform(MyRotator, MyVector, FVector(1.0f));
Concurrency
Rather than std::thread and std::async, Unreal Engine provides its own concurrency primitives built on the engine’s task graph and thread pool.
Async: Utility function for running a task asynchronously and getting a TFuture back.
TFuture<int32> Result = Async(EAsyncExecution::ThreadPool, []()
{
// Task code here
return 42;
});
FGraphEventRef: Handles task dependencies and event-driven execution on the task graph.
FGraphEventRef MyEvent = FFunctionGraphTask::CreateAndDispatchWhenReady([]()
{
// Task code here
});
FRunnable: Interface for creating dedicated threads.
class FMyRunnable : public FRunnable
{
public:
virtual uint32 Run() override
{
// Thread code here
return 0;
}
};
// Launch on a new thread:
FRunnableThread* Thread = FRunnableThread::Create(new FMyRunnable(), TEXT("MyThread"));
The Unreal Build System
The Unreal Build Tool (UBT) is a powerful, flexible build system designed specifically for Unreal Engine projects. It manages compilation and linking, handles module dependencies, and ensures your project builds correctly for each target platform. Understanding it is essential for managing large projects.
Build Configuration
UBT uses C# configuration files to define how projects are built. The primary files you’ll interact with are .Build.cs and .Target.cs.
.Build.cs files define a module’s dependencies, build settings, and include paths:
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
.Target.cs files define the build targets for your project, such as the game and editor targets:
public class MyGameTarget : TargetRules
{
public MyGameTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V2;
ExtraModuleNames.AddRange(new string[] { "MyGame" });
}
}
Build configurations: UBT supports several build configurations, each optimized for a different stage of development:
- Debug / DebugGame: Full (or game-module-only) debug symbols and no optimization, for stepping through code.
- Development: The default for day-to-day work — optimized but still debuggable.
- Test: Shipping with some console commands and testing features enabled.
- Shipping: Fully optimized for distribution, with development-only features stripped.
Build Phases
The build process in Unreal Engine consists of several phases:
- Pre-build: UBT gathers information about the project’s modules, dependencies, and configuration (and UHT generates reflection code).
- Compile: Source files are compiled into object files.
- Link: Object files are linked into the final binaries.
- Post-build: Additional tasks run, such as copying files and staging assets.
Packaging and Deployment
Unreal Engine simplifies packaging and deploying projects. Packaging prepares your project for distribution, making sure all necessary files are included and configured correctly.
Packaging for different platforms: Unreal Engine supports Windows, macOS, Linux, iOS, Android, and consoles. Platform-specific settings can live right in your target rules:
public class MyGameTarget : TargetRules
{
public MyGameTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V2;
ExtraModuleNames.AddRange(new string[] { "MyGame" });
if (Target.Platform == UnrealTargetPlatform.Win64)
{
// Windows-specific build settings
}
else if (Target.Platform == UnrealTargetPlatform.Android)
{
// Android-specific build settings
}
}
}
Automated builds: The Unreal Automation Tool (UAT) automates building, cooking, testing, and packaging:
RunUAT.bat BuildCookRun -project="MyProject.uproject" -noP4 -platform=Win64 -clientconfig=Shipping -cook -allmaps -build -stage -pak -archive -archivedirectory="C:/MyProjectBuild"
Command line interface: Build and cook steps can also be driven directly from the command line, which makes CI/CD integration straightforward. Note that in UE5 the editor binary is UnrealEditor-Cmd.exe (the old UE4Editor-Cmd.exe name is gone):
UnrealEditor-Cmd.exe MyProject.uproject -run=Cook -targetplatform=Windows
Development Workflow
Developing in Unreal Engine involves a distinct workflow that integrates code, assets, and the Unreal Editor.
Editor Scripting
The Unreal Editor provides powerful scripting capabilities for automating repetitive tasks, extending editor functionality, and customizing your development environment.
Editor modules: Editor-specific code is typically placed in a separate module from runtime code, with UnrealEd (and for UI work, Slate and SlateCore) among its dependencies:
public class MyEditorModule : ModuleRules
{
public MyEditorModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "UnrealEd", "Slate", "SlateCore" });
}
}
Custom editor windows: You can create custom editor tabs and windows using the Slate UI framework:
void FMyEditorModule::StartupModule()
{
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(MyTabName,
FOnSpawnTab::CreateRaw(this, &FMyEditorModule::OnSpawnPluginTab))
.SetDisplayName(NSLOCTEXT("MyEditorModule", "TabTitle", "My Editor Tab"))
.SetMenuType(ETabSpawnerMenuType::Hidden);
}
TSharedRef<SDockTab> FMyEditorModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
SNew(STextBlock).Text(NSLOCTEXT("MyEditorModule", "TabText", "Hello, Unreal Editor!"))
];
}
Automating tasks with Python: Unreal Engine supports Python scripting for automating editor tasks and workflows:
import unreal
def prefix_assets(asset_path, prefix):
assets = unreal.EditorAssetLibrary.list_assets(asset_path)
for asset in assets:
asset_name = asset.split("/")[-1].split(".")[0]
new_path = asset.rsplit("/", 1)[0] + "/" + prefix + asset_name
unreal.EditorAssetLibrary.rename_asset(asset, new_path)
prefix_assets("/Game/MyAssets", "SM_")
Debugging and Profiling
Efficient debugging and profiling are crucial for identifying and resolving issues in Unreal Engine projects.
Visual Studio integration: Unreal Engine integrates with Visual Studio for seamless C++ debugging. Set breakpoints, inspect variables, and step through code — either by launching the editor under the debugger or by attaching to the running editor process:
void AMyActor::BeginPlay()
{
Super::BeginPlay();
// Set a breakpoint here
UE_LOG(LogTemp, Warning, TEXT("Hello, Unreal!"));
}
Unreal Insights: A powerful profiling tool that captures CPU and GPU performance, memory usage, and asset loading times. You can add your own trace scopes to instrument specific functions:
void MyFunction()
{
TRACE_CPUPROFILER_EVENT_SCOPE(MyFunction);
// Function code
}
Logging: Use the UE_LOG macro to write messages to the Output Log and log files:
UE_LOG(LogTemp, Warning, TEXT("This is a warning message"));
UE_LOG(LogTemp, Error, TEXT("This is an error message"));
Assertions: Use the check and ensure macros to verify assumptions and catch errors early. check halts execution when the condition fails; ensure logs and continues, triggering the debugger only once.
check(MyPointer != nullptr);
ensure(MyValue > 0);
Version Control
Managing code and assets with version control is essential for any Unreal Engine project. Unreal Engine integrates with Git, Perforce, and Subversion.
Git: Works well for code, but Unreal projects also contain large binary assets, so a careful .gitignore (and usually Git LFS for .uasset/.umap files) is important:
# Unreal Engine
Binaries/
DerivedDataCache/
Intermediate/
Saved/
# Visual Studio
.vs/
*.suo
*.vcxproj.user
*.VC.db
Perforce: Widely used in larger teams and studios for its robust handling of large binary files and exclusive checkouts. Configure it in the Unreal Editor under Edit > Editor Preferences > Source Control:
P4PORT=perforce.example.com:1666
P4USER=myusername
P4CLIENT=myworkspace
Best Practices
Developing in Unreal Engine 5 with C++ can be complex, but following a few best practices will help you write efficient, maintainable, and performant code.
Memory Management
Use UPROPERTY for UObject pointers: Always mark pointers to UObjects with UPROPERTY so the garbage collector can track them:
UPROPERTY()
UMyObject* MyObject;
Avoid raw pointers for non-UObject types: For regular C++ objects, prefer Unreal’s smart pointers (TSharedPtr, TWeakPtr, TUniquePtr) over raw new/delete:
TSharedPtr<FMyClass> MySharedPtr = MakeShared<FMyClass>();
Consider object lifetimes: When creating UObjects, pass an appropriate Outer so the object’s lifetime is tied to its logical owner:
UMyObject* MyObject = NewObject<UMyObject>(this);
Code Organization
Modularize your code: Use modules to separate different functionality and keep your codebase organized:
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
}
}
Follow naming conventions: Adhere to Unreal Engine’s conventions for consistency and readability:
- Classes:
Uprefix for UObject-derived classes,Afor actors,Ffor plain structs and classes,Efor enums. - Functions and variables: PascalCase (e.g.,
MyFunction,PlayerHealth). - Booleans:
bprefix (e.g.,bIsAlive).
Use forward declarations: Forward-declare types in headers where possible to reduce compile times and dependencies:
class UMyClass;
Performance Optimization
Profile early and often: Use Unreal Insights and the other profiling tools to find real bottlenecks instead of guessing:
void MyFunction()
{
TRACE_CPUPROFILER_EVENT_SCOPE(MyFunction);
// Function code
}
Minimize Tick functions: Avoid unnecessary per-frame work. Use events, delegates, and timers instead:
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyActor::MyFunction, 1.0f, true);
Preallocate memory: Use TArray::Reserve to preallocate capacity and reduce reallocations:
TArray<int32> MyArray;
MyArray.Reserve(100);
Use efficient data structures: Choose the right container for the job — TMap and TSet give you fast hash-based lookups and uniqueness guarantees:
TMap<FString, int32> MyMap;
MyMap.Add(TEXT("Key"), 100);
Debugging and Error Handling
Log liberally during development: UE_LOG output is invaluable when tracking down issues:
UE_LOG(LogTemp, Warning, TEXT("This is a warning message"));
Verify assumptions: Use check for conditions that must never fail and ensure for recoverable ones:
check(MyPointer != nullptr);
ensure(MyValue > 0);
Further Learning
To master Unreal Engine 5 C++ development, take advantage of these resources:
- Unreal Engine Documentation: The official documentation is a comprehensive resource covering all aspects of the engine, including tutorials, API references, and best practices.
- Unreal Engine Learning: Epic offers a series of free, on-demand courses and tutorials covering topics from beginner to advanced.
- Udemy courses: Numerous Unreal Engine courses at every level.
- YouTube channels: The official Unreal Engine channel, plus community channels like Smart Poly, Bad Decisions Studio, and Ryan Laley.
Conclusion
Unreal Engine 5 extends and adapts standard C++ to meet the demands of game development: a reflection system and garbage collector that standard C++ lacks, engine-specific containers and strings, a modular build system, and a full gameplay framework. The syntax is still C++, but the idioms are Unreal’s own. By understanding where the two diverge — and following the engine’s conventions rather than fighting them — you can leverage everything UE5 offers and write high-quality, performant games and simulations.