Skip to main content

04 / 16

Functions and results

Make the signature show what's required, what's optional, and who owns the result.

The signature is the first thing a caller reads, and often the only thing. Positional booleans and bare output parameters make the reader work it out for themselves.

Rules

  1. 01Pass small scalar and enum values by value.
  2. 02Pass larger read-only values by const reference.
  3. 03Use a pointer when null has a defined meaning. Use a reference when the value is required.
  4. 04Keep one boolean setter when it represents the complete state. Replace behavior flags once they become modes or combinations.
  5. 05Take TConstArrayView<T> when a function only reads a contiguous range. It binds to a TArray, a C array, or an initializer list without copying, so callers stop converting containers at the boundary.
  6. 06Use TFunctionRef for a callback the function only invokes during its own execution, and TFunction when it stores the callback for later. TFunctionRef does not own the callable, so keeping one past the call is a dangling reference.
  7. 07Mark a query [[nodiscard]] when ignoring the result is a bug. Unreal already applies it across the containers, and it turns a silent mistake into a compiler warning.
  8. 08Put the unit in the parameter name rather than a comment. A caller reading SetCooldown(2.0f) has no way to tell seconds from milliseconds.

Result shapes

Direct return

The function produces one required value. Simple to call and easy to compose with other functions.

CostsAn empty result needs to be part of the returned type itself.

TOptional<T>

One value may be absent during normal operation. Makes the caller handle the possibility of no value.

CostsDoes not explain why the value is missing when several failures are possible.

Result struct

Several returned values belong together, or the result will gain more fields. Keeps related values and status in one named type.

CostsIntroduces another type.

Output parameters

You are matching an existing API, or returning optional values that are independent. Writes directly into storage supplied by the caller.

CostsThe caller can read partially written output unless the function initializes every value.

Return related values together

The result keeps the actor and its distance together, while TOptional handles the case where no target was found.

TargetingLibrary.cppcpp
struct FTargetSearchResult
{
    AActor* Target = nullptr;
    float Distance = 0.0f;
};

TOptional<FTargetSearchResult> FindClosestTarget(
    const FVector& Origin,
    const TArray<TObjectPtr<AActor>>& Candidates)
{
    AActor* ClosestTarget = nullptr;
    float ClosestDistanceSquared = TNumericLimits<float>::Max();

    for (AActor* Candidate : Candidates)
    {
        if (!IsValid(Candidate))
        {
            continue;
        }

        const float DistanceSquared = FVector::DistSquared(
            Origin,
            Candidate->GetActorLocation());

        if (DistanceSquared < ClosestDistanceSquared)
        {
            ClosestTarget = Candidate;
            ClosestDistanceSquared = DistanceSquared;
        }
    }

    if (ClosestTarget == nullptr)
    {
        return {};
    }

    return FTargetSearchResult{
        ClosestTarget,
        FMath::Sqrt(ClosestDistanceSquared)
    };
}
Why
The caller cannot read a distance when no target exists or forget to initialize an output variable. You can also add another result field later without changing every call site.
Costs
A result struct adds another type. That is unnecessary when a function returns one required scalar.
Instead
Use a direct value return for one mandatory result. Use output parameters when matching an established Unreal API or when each output is independently optional.

Group optional settings in a parameter struct

Each setting has a name and a default instead of becoming another positional argument.

ProjectileSpawnParams.hcpp
USTRUCT(BlueprintType)
struct FProjectileSpawnParams
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Speed = 3000.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Damage = 25.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    bool bInheritOwnerVelocity = true;
};

AProjectile* SpawnProjectile(
    UWorld& World,
    const FTransform& SpawnTransform,
    const FProjectileSpawnParams& Params);
Why
Callers can change only the fields they need. Adding another setting does not reorder arguments or break existing calls.
Costs
The function accepts combinations that may not make sense. Validate cross-field rules at the boundary.
Instead
Keep separate parameters when there are only a few, every value is required, and the order is obvious. Builders are useful when construction has distinct stages or invariants.

Replace behavior booleans with a named type

Flags make this call readable without forcing you to look up what several boolean arguments mean.

TargetFilter.hcpp
UENUM(BlueprintType, meta = (Bitflags))
enum class ETargetFilter : uint8
{
    None = 0,
    Alive = 1 << 0,
    Hostile = 1 << 1,
    Visible = 1 << 2
};
ENUM_CLASS_FLAGS(ETargetFilter)

TArray<AActor*> FindTargets(
    const FVector& Origin,
    float Radius,
    ETargetFilter Filter);

const TArray<AActor*> Targets = FindTargets(
    GetActorLocation(),
    1500.0f,
    ETargetFilter::Alive | ETargetFilter::Hostile);
Why
The compiler rejects unrelated enum values, and a call states exactly which filters it applies.
Costs
Bit flags permit combinations. If only one mode is valid at a time, use an ordinary enum class without ENUM_CLASS_FLAGS.
Instead
A single setter such as SetEnabled(bool bEnabled) is already clear. Introduce an enum when booleans select behavior or start to multiply.

All 16 rules