Last active
November 30, 2025 14:47
-
-
Save Solessfir/e94214e648c69bf5167497e108ab9580 to your computer and use it in GitHub Desktop.
C++23 std::print-style Single Header Logging library for Unreal Engine 4 - 5.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Copyright (c) Solessfir under MIT license | |
| * | |
| * #define EASY_LOG_CATEGORY LogMyGame // Optional (must be defined before include) | |
| * #include "EasyLog.h" | |
| * | |
| * Usage Examples: | |
| * | |
| * // Basic | |
| * LOG_DISPLAY("Value is {}", MyInt); | |
| * | |
| * // Extended (Key, Duration) | |
| * LOG_DISPLAY_EX(-1, 5.f, "Value is {}", MyInt); | |
| * | |
| * // Positional args | |
| * LOG_DISPLAY("Value is {1}, expected {0}", Foo, Bar); // Value is Bar, expected Foo | |
| * | |
| * // Formatting Specifiers | |
| * LOG_DISPLAY("Int: {:03}", 7); // Output: Int: 007 | |
| * LOG_DISPLAY("Float: {:.2}", 3.14159); // Output: Float: 3.14 | |
| * | |
| * // Hex and Binary | |
| * int32 Val = 255; | |
| * LOG_DISPLAY("Hex: {:#x}", Val); // Output: Hex: 0xff | |
| * LOG_DISPLAY("Bin: {:#b}", Val); // Output: Bin: 0b11111111 | |
| * | |
| * // Pointer Address | |
| * LOG_DISPLAY("Ptr: {:#x}", this); // Output: Ptr: 0x00... | |
| * | |
| * // Container Support (TArray, TSet) | |
| * TArray<float> Values = {1.11f, 2.22f}; | |
| * LOG_DISPLAY("Values: {:.1}", Values); // Output: Values: [1.1, 2.2] | |
| * | |
| * // UEnum and FGameplayTag Support | |
| * LOG_DISPLAY("State: {}", EMyEnum::Walking); | |
| * LOG_DISPLAY("Tag: {}", MyTag); | |
| * | |
| * // Conditional Logging | |
| * CLOG_ERROR(Health <= 0, "Actor {} died!", this); | |
| * | |
| * For UE4 - add these lines to your .Target.cs: | |
| * bOverrideBuildEnvironment = true; | |
| * CppStandard = CppStandardVersion.Cpp17; | |
| */ | |
| #pragma once | |
| #include "CoreMinimal.h" | |
| #include "Engine/Engine.h" | |
| #include <type_traits> | |
| #include <cstdarg> | |
| #ifndef EASY_LOG_CATEGORY | |
| #define EASY_LOG_CATEGORY EasyLog | |
| #endif | |
| // Helper to expand macro before definition | |
| #define EASY_LOG_DEFINE_CATEGORY_INTERNAL(Category) DEFINE_LOG_CATEGORY_STATIC(Category, Log, All) | |
| EASY_LOG_DEFINE_CATEGORY_INTERNAL(EASY_LOG_CATEGORY); | |
| #undef EASY_LOG_DEFINE_CATEGORY_INTERNAL | |
| // Helper to expand macro for Usage | |
| #define EASY_UE_LOG_EXPAND(Category, Verbosity, Format, ...) UE_LOG(Category, Verbosity, Format, ##__VA_ARGS__) | |
| #if defined(_MSC_VER) | |
| #define EASY_FUNC_SIG __FUNCTION__ | |
| #else | |
| #define EASY_FUNC_SIG __PRETTY_FUNCTION__ | |
| #endif | |
| #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) | |
| #define GET_LOG_LOC Easy::FormatLocation(EASY_FUNC_SIG, __LINE__) | |
| #define LOG_KEY_HASH static_cast<int32>((FCrc::MemCrc32(__FUNCTION__, sizeof(__FUNCTION__) - 1) ^ (static_cast<uint32>(__LINE__) << 15)) & 0x7FFFFFFF) | |
| #define LOG_DISPLAY(Fmt, ...) Easy::Dispatch<ELogVerbosity::Display>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| #define LOG_WARNING(Fmt, ...) Easy::Dispatch<ELogVerbosity::Warning>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| #define LOG_ERROR(Fmt, ...) Easy::Dispatch<ELogVerbosity::Error>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__) | |
| #define CLOG_DISPLAY(Cond, Fmt, ...) if (Cond) LOG_DISPLAY(Fmt, ##__VA_ARGS__) | |
| #define CLOG_WARNING(Cond, Fmt, ...) if (Cond) LOG_WARNING(Fmt, ##__VA_ARGS__) | |
| #define CLOG_ERROR(Cond, Fmt, ...) if (Cond) LOG_ERROR(Fmt, ##__VA_ARGS__) | |
| #define LOG_DISPLAY_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Display>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #define LOG_WARNING_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Warning>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #define LOG_ERROR_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Error>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__) | |
| #else | |
| #define LOG_DISPLAY(...) | |
| #define LOG_WARNING(...) | |
| #define LOG_ERROR(...) | |
| #define CLOG_DISPLAY(...) | |
| #define CLOG_WARNING(...) | |
| #define CLOG_ERROR(...) | |
| #define LOG_DISPLAY_EX(...) | |
| #define LOG_WARNING_EX(...) | |
| #define LOG_ERROR_EX(...) | |
| #endif | |
| namespace Easy | |
| { | |
| template <typename T> using Decayed = std::decay_t<T>; | |
| // SFINAE Helpers | |
| template <typename T, typename = void> struct THasToString : std::false_type {}; | |
| template <typename T> struct THasToString<T, std::void_t<decltype(std::declval<T>().ToString())>> : std::true_type {}; | |
| template <typename T, typename = void> struct TIsContainer : std::false_type {}; | |
| template <typename T> struct TIsContainer<T, std::void_t<decltype(std::declval<T>().begin()), decltype(std::declval<T>().end())>> : std::true_type {}; | |
| // Location Helper | |
| inline FString FormatLocation(const char* InFunc, const int32 Line) | |
| { | |
| FString Result(InFunc); | |
| if (Result.Contains(TEXT("("))) Result = Result.Left(Result.Find(TEXT("("))); | |
| int32 SpaceIdx = -1; | |
| if (Result.FindLastChar(TEXT(' '), SpaceIdx)) Result = Result.RightChop(SpaceIdx + 1); | |
| return FString::Printf(TEXT("%s:%d"), *Result, Line); | |
| } | |
| struct FParsedFormat | |
| { | |
| FString FixedString; | |
| TArray<FString> Specifiers; | |
| }; | |
| struct FormatStringHelper | |
| { | |
| static FParsedFormat Parse(const FString& InFormat) | |
| { | |
| if (!InFormat.Contains(TEXT("{"))) | |
| { | |
| return { InFormat, {} }; | |
| } | |
| FParsedFormat Result; | |
| Result.FixedString.Reserve(InFormat.Len() + 16); | |
| int32 ArgIndex = 0; | |
| const int32 Len = InFormat.Len(); | |
| for (int32 i = 0; i < Len; ++i) | |
| { | |
| const TCHAR Char = InFormat[i]; | |
| if (Char == '{') | |
| { | |
| if (i + 1 < Len && InFormat[i + 1] == '{') | |
| { | |
| Result.FixedString.AppendChar('{'); Result.FixedString.AppendChar('{'); i++; | |
| continue; | |
| } | |
| int32 CloseIdx = -1; | |
| for (int32 j = i + 1; j < Len; ++j) | |
| { | |
| if (InFormat[j] == '}') | |
| { | |
| CloseIdx = j; | |
| break; | |
| } | |
| } | |
| if (CloseIdx != -1) | |
| { | |
| FString Content = InFormat.Mid(i + 1, CloseIdx - (i + 1)); | |
| FString Specifier; | |
| int32 ColonIdx; | |
| // Extract specifier (e.g. "02" from "{:02}" or "{0:02}") | |
| if (Content.FindChar(TEXT(':'), ColonIdx)) | |
| { | |
| Specifier = Content.RightChop(ColonIdx + 1); | |
| Specifier.TrimStartAndEndInline(); | |
| Content = Content.Left(ColonIdx); | |
| } | |
| if (Content.IsEmpty()) | |
| { | |
| // Case: {} or {:spec} -> Auto-Increment Index | |
| Result.FixedString.AppendChar('{'); Result.FixedString.AppendInt(ArgIndex); Result.FixedString.AppendChar('}'); | |
| if (Result.Specifiers.Num() <= ArgIndex) Result.Specifiers.SetNum(ArgIndex + 1); | |
| Result.Specifiers[ArgIndex] = Specifier; | |
| ArgIndex++; | |
| } | |
| else if (Content.IsNumeric()) | |
| { | |
| // Case: {0} or {0:spec} -> Explicit Index | |
| Result.FixedString.AppendChar('{'); Result.FixedString += Content; Result.FixedString.AppendChar('}'); | |
| const int32 ExplicitIdx = FCString::Atoi(*Content); | |
| if (Result.Specifiers.Num() <= ExplicitIdx) Result.Specifiers.SetNum(ExplicitIdx + 1); | |
| Result.Specifiers[ExplicitIdx] = Specifier; | |
| } | |
| else | |
| { | |
| // Unsupported Case: {Name} -> Treat as literal text | |
| // Append the entire original block including braces | |
| Result.FixedString.AppendChars(&InFormat[i], CloseIdx - i + 1); | |
| } | |
| i = CloseIdx; | |
| continue; | |
| } | |
| } | |
| Result.FixedString.AppendChar(Char); | |
| } | |
| return Result; | |
| } | |
| }; | |
| struct SafeFormatter | |
| { | |
| static FString RunFormat(const TCHAR* Fmt, ...) | |
| { | |
| TCHAR Buffer[128]; | |
| va_list Ap; | |
| va_start(Ap, Fmt); | |
| FCString::GetVarArgs(Buffer, 128, Fmt, Ap); | |
| va_end(Ap); | |
| return FString(Buffer); | |
| } | |
| static FString ToBinary(const int64 Value, const bool bUsePrefix) | |
| { | |
| FString Result; | |
| uint64 UVal = static_cast<uint64>(Value); | |
| if (UVal == 0) | |
| { | |
| Result = TEXT("0"); | |
| } | |
| else | |
| { | |
| while (UVal > 0) | |
| { | |
| Result = (UVal & 1 ? TEXT("1") : TEXT("0")) + Result; | |
| UVal >>= 1; | |
| } | |
| } | |
| return bUsePrefix ? TEXT("0b") + Result : Result; | |
| } | |
| static FString FormatInt(const int64 Value, const FString& Spec) | |
| { | |
| if (Spec.EndsWith(TEXT("b"), ESearchCase::IgnoreCase)) | |
| { | |
| return ToBinary(Value, Spec.Contains(TEXT("#"))); | |
| } | |
| if (Spec.EndsWith(TEXT("x"), ESearchCase::IgnoreCase)) | |
| { | |
| FString HexSpec = Spec; | |
| if (HexSpec.Contains(TEXT("x"))) HexSpec.ReplaceInline(TEXT("x"), TEXT("llx")); | |
| else if (HexSpec.Contains(TEXT("X"))) HexSpec.ReplaceInline(TEXT("X"), TEXT("llX")); | |
| const FString Fmt = TEXT("%") + HexSpec; | |
| return RunFormat(*Fmt, Value); | |
| } | |
| const FString Fmt = TEXT("%") + Spec + TEXT("lld"); | |
| return RunFormat(*Fmt, Value); | |
| } | |
| static FString FormatFloat(const double Value, const FString& Spec) | |
| { | |
| const FString Fmt = TEXT("%") + Spec + TEXT("f"); | |
| return RunFormat(*Fmt, Value); | |
| } | |
| }; | |
| // Native FStringArg | |
| template <typename T, typename std::enable_if_t<std::is_constructible<FStringFormatArg, T>::value && !TIsContainer<Decayed<T>>::value, int> = 0> | |
| void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString& Spec) | |
| { | |
| if constexpr (std::is_pointer<Decayed<T>>::value) | |
| { | |
| // Check for explicit format (x, p, etc) - This catches 'this' pointers too! | |
| if (!Spec.IsEmpty()) { Args.Emplace(SafeFormatter::FormatInt(reinterpret_cast<int64>(Val), Spec)); return; } | |
| // TCHAR* safety | |
| if constexpr (std::is_same<typename std::remove_pointer<Decayed<T>>::type, TCHAR>::value) | |
| { | |
| if (Val == nullptr) | |
| { | |
| Args.Emplace(TEXT("(NullString)")); | |
| return; | |
| } | |
| } | |
| } | |
| if constexpr (std::is_floating_point<Decayed<T>>::value) | |
| { | |
| if (!Spec.IsEmpty()) Args.Emplace(SafeFormatter::FormatFloat(static_cast<double>(Val), Spec)); | |
| else Args.Emplace(FString::SanitizeFloat(Val)); | |
| } | |
| else if constexpr (std::is_integral<Decayed<T>>::value) | |
| { | |
| if (!Spec.IsEmpty()) Args.Emplace(SafeFormatter::FormatInt(static_cast<int64>(Val), Spec)); | |
| else Args.Emplace(Val); | |
| } | |
| else | |
| { | |
| Args.Emplace(Val); | |
| } | |
| } | |
| // Literal nullptr | |
| inline void Append(FStringFormatOrderedArguments& Args, std::nullptr_t, const FString&) | |
| { | |
| Args.Emplace(TEXT("nullptr")); | |
| } | |
| // ToString() | |
| template <typename T, typename std::enable_if_t<THasToString<Decayed<T>>::value && !std::is_constructible<FStringFormatArg, T>::value, int> = 0> | |
| void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString&) | |
| { | |
| Args.Emplace(Val.ToString()); | |
| } | |
| // UObject/Actor | |
| template <typename T, typename std::enable_if_t<std::is_pointer<T>::value && std::is_base_of<UObject, std::remove_pointer_t<T>>::value, int> = 0> | |
| void Append(FStringFormatOrderedArguments& Args, T Val, const FString& Spec) | |
| { | |
| // If formatting is present (e.g. {:#x}), treat as raw pointer address | |
| if (!Spec.IsEmpty()) | |
| { | |
| Args.Emplace(SafeFormatter::FormatInt(reinterpret_cast<int64>(Val), Spec)); | |
| return; | |
| } | |
| if (IsInGameThread() && IsValid(Val)) Args.Emplace(Val->GetName()); | |
| else Args.Emplace(IsValid(Val) ? TEXT("[AsyncUObject]") : TEXT("None")); | |
| } | |
| // Enums | |
| template <typename T, typename std::enable_if_t<std::is_enum<Decayed<T>>::value, int> = 0> | |
| void Append(FStringFormatOrderedArguments& Args, T Val, const FString& Spec) | |
| { | |
| // If formatting is present, treat as int | |
| if (!Spec.IsEmpty()) | |
| { | |
| Args.Emplace(SafeFormatter::FormatInt(static_cast<int64>(Val), Spec)); | |
| return; | |
| } | |
| const UEnum* EnumPtr = StaticEnum<Decayed<T>>(); | |
| if (EnumPtr) Args.Emplace(EnumPtr->GetNameStringByValue(static_cast<int64>(Val))); | |
| else Args.Emplace(static_cast<int64>(Val)); | |
| } | |
| // Containers | |
| template <typename T, typename std::enable_if_t<TIsContainer<Decayed<T>>::value && !std::is_same<Decayed<T>, FString>::value, int> = 0> | |
| void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString& Spec) | |
| { | |
| FString Result = TEXT("["); | |
| int32 Count = 0; | |
| constexpr int32 MaxElements = 15; | |
| for (const auto& Elem : Val) | |
| { | |
| if (Count > 0) Result += TEXT(", "); | |
| if (Count >= MaxElements) { Result += TEXT("..."); break; } | |
| if constexpr (std::is_floating_point<Decayed<decltype(Elem)>>::value) | |
| { | |
| if (!Spec.IsEmpty()) Result += SafeFormatter::FormatFloat(static_cast<double>(Elem), Spec); | |
| else Result += FString::SanitizeFloat(Elem); | |
| } | |
| else if constexpr (std::is_integral<Decayed<decltype(Elem)>>::value) | |
| { | |
| if (!Spec.IsEmpty()) Result += SafeFormatter::FormatInt(static_cast<int64>(Elem), Spec); | |
| else Result += LexToString(Elem); | |
| } | |
| else | |
| { | |
| Result += LexToString(Elem); | |
| } | |
| Count++; | |
| } | |
| Result += TEXT("]"); | |
| Args.Emplace(Result); | |
| } | |
| // Fallback | |
| inline void Append(FStringFormatOrderedArguments& Args, ...) { Args.Emplace(TEXT("[?]")); } | |
| inline void Fill(FStringFormatOrderedArguments&, const TArray<FString>&, int32) {} | |
| template<typename FirstArgType, typename... RestArgsType> | |
| void Fill(FStringFormatOrderedArguments& Args, const TArray<FString>& Specs, const int32 Index, FirstArgType&& First, RestArgsType&&... Rest) | |
| { | |
| FString CurrentSpec = Specs.IsValidIndex(Index) ? Specs[Index] : FString(); | |
| Append(Args, Forward<FirstArgType>(First), CurrentSpec); | |
| Fill(Args, Specs, Index + 1, Forward<RestArgsType>(Rest)...); | |
| } | |
| template <ELogVerbosity::Type Verbosity, typename... ArgsType> | |
| void Dispatch(const int32 Key, const float Duration, const FString& Location, const FString& Format, ArgsType&&... Arguments) | |
| { | |
| const FParsedFormat Parsed = FormatStringHelper::Parse(Format); | |
| FStringFormatOrderedArguments OrderedArgs; | |
| Fill(OrderedArgs, Parsed.Specifiers, 0, Forward<ArgsType>(Arguments)...); | |
| const FString UserMessage = FString::Format(*Parsed.FixedString, MoveTemp(OrderedArgs)); | |
| const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Location); | |
| switch (Verbosity) | |
| { | |
| case ELogVerbosity::Fatal: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Fatal, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Error: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Error, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Warning: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Warning, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Display: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Display, TEXT("%s"), *FullMessage); break; | |
| case ELogVerbosity::Verbose: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Verbose, TEXT("%s"), *FullMessage); break; | |
| default: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Log, TEXT("%s"), *FullMessage); break; | |
| } | |
| if (GEngine && Duration > 0.f) | |
| { | |
| const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White; | |
| GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment