Skip to main content

11 / 16

Delegates, timers, and deferred work

Bind deferred callbacks with something that understands the owner's lifetime.

Timers, delegates, and async callbacks can fire after their owner is gone. A raw this capture has no way to know that.

Rules

  1. 01Use BindUObject or CreateUObject for UObject member functions.
  2. 02Use CreateWeakLambda or AddWeakLambda for a lambda owned by a UObject.
  3. 03Use BindSP or CreateSPLambda for a plain C++ owner held by a shared pointer.
  4. 04A raw binding is valid only when another contract proves the target outlives the delegate. Document that contract beside the binding.
  5. 05Use ExecuteIfBound for void delegates. Check IsBound before executing a delegate with return or output values.
  6. 06On a worker thread, pin a TWeakObjectPtr to a TStrongObjectPtr before UObject access. Direct TObjectPtr access is unsafe unless another contract keeps the object rooted.
  7. 07Dynamic delegates bind by function name through AddDynamic, BindDynamic, or BindUFunction, so the target has to be a UFUNCTION. Getting it wrong fails at runtime when the bind happens, not at compile time.
  8. 08Reach for the DECLARE_DYNAMIC_ families only when Blueprint needs to bind. They serialize and dispatch by name, which costs more than the plain DECLARE_DELEGATE_ families.
  9. 09Use CreateThreadSafeSP when the delegate can execute off the game thread. The plain SP variants use non-atomic reference counting and will race.
  10. 10Unbind in EndPlay rather than the destructor. A delegate owned by a longer-lived object keeps firing into an actor that has already left play, and the destructor runs too late to prevent it.

Use a weak binding for delayed UObject work

The timer tracks the component weakly instead of keeping an unchecked raw this capture.

WeaponComponent.cppcpp
void UWeaponComponent::StartCooldown(const float DurationSeconds)
{
    GetWorld()->GetTimerManager().SetTimer(
        CooldownTimer,
        FTimerDelegate::CreateWeakLambda(this, [this]()
        {
            bIsOnCooldown = false;
            OnCooldownEnded.Broadcast();
        }),
        DurationSeconds,
        false);

    bIsOnCooldown = true;
}
Why
If the component is destroyed before the timer fires, Unreal skips the callback instead of dereferencing freed memory.
Costs
The skipped callback is silent. If another system waits for completion, the owner still needs a cancellation path.
Instead
Use BindUObject for a member function, CreateSPLambda for a shared plain C++ owner, or an explicit TWeakObjectPtr capture when the callback needs several objects.

All 16 rules