본문 바로가기
개발이야기/언리얼 c++

언리얼 C++: Lambda와 mutable Lambda의 차이

by oddsilk 2024. 8. 6.

람다 함수는 기본적으로 상수로 캡처한 변수를 수정할 수 없습니다. 이를 해결하기 위해 mutable 키워드를 사용하여 람다 함수를 변경할 수 있게 만듭니다.

람다와 mutable 람다의 차이

기본 람다

기본적으로 람다 함수는 캡처된 변수를 상수로 취급합니다. 즉, 람다 함수 내에서 캡처된 변수를 수정할 수 없습니다.

int x = 0;
auto lambda = [x]() {
    // x++; // 오류: x는 상수로 캡처되었기 때문에 수정할 수 없습니다.
    std::cout << x << std::endl;
};

lambda();

mutable 람다

mutable 키워드를 사용하면, 람다 함수 내에서 캡처된 변수를 수정할 수 있습니다. mutable 키워드를 사용하면 캡처된 변수는 상수가 아닌 일반 변수로 취급됩니다.

int x = 0;
auto lambda = [x]() mutable {
    x++; // 가능: mutable 키워드를 사용했기 때문에 x를 수정할 수 있습니다.
    std::cout << x << std::endl;
};

lambda();

mutable 람다의 예제

mutable 람다는 캡처된 변수를 람다 함수 내부에서 변경할 수 있도록 합니다. 이 기능을 사용할 때는 다음과 같은 상황을 고려해야 합니다.

 

 

#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    int factor = 2;

    // 기본 람다: factor를 수정할 수 없음
    std::for_each(numbers.begin(), numbers.end(), [factor](int &n) {
        // factor++; // 오류: factor는 상수로 캡처되었기 때문에 수정할 수 없음
        n *= factor;
    });

    // mutable 람다: factor를 수정할 수 있음
    std::for_each(numbers.begin(), numbers.end(), [factor]() mutable {
        factor++; // 가능: mutable 키워드를 사용했기 때문에 factor를 수정할 수 있음
    });

    // 결과 출력
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

이 예제에서는 factor 변수를 mutable 키워드를 사용하여 람다 함수 내부에서 수정할 수 있게 했습니다.

Unreal Engine에서의 mutable 람다 사용

Unreal Engine에서 람다 함수와 FTimerManager를 사용하여 타이머 콜백을 설정할 때도 mutable 키워드를 사용할 수 있습니다. 특히, 타이머 콜백 내에서 상태를 변경해야 할 경우 유용합니다.

Unreal Engine 예제

다음은 mutable 람다를 사용하여 타이머 콜백 내에서 캡처된 변수를 수정하는 예제입니다.

 

#include "YourClass.h"
#include "Engine/World.h"
#include "TimerManager.h"

// Sets default values
AYourClass::AYourClass()
{
    PrimaryActorTick.bCanEverTick = true;

    // Initialize variables
    SequentialIndex = 0;
    DelayBetweenSequentialSpawns = 0.5f;
    TimeBetweenSpawns = 5.0f; // Example value
    SpawnStyle = ESpawnStyle::Sequential; // Default value
}

// Called when the game starts or when spawned
void AYourClass::BeginPlay()
{
    Super::BeginPlay();

    // Start the sequential spawn
    StartSequentialSpawn();
}

// Called every frame
void AYourClass::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void AYourClass::StartSequentialSpawn()
{
    // Define the initial spawn start callback using FTimerDelegate
    FTimerDelegate SequentialSpawnStartCallback;

    SequentialSpawnStartCallback = FTimerDelegate::CreateLambda([this]()
    {
        switch (SpawnStyle)
        {
            case ESpawnStyle::Sequential:
                SequentialIndex = 0;
                SequentialSpawn();
                break;
            case ESpawnStyle::ReverseSequential:
                ReverseIndex = SelectedPointIndexList.Num() - 1;
                ReverseSequentialSpawn();
                break;
            case ESpawnStyle::MiddleOut:
                MiddleOutSpawn();
                break;
            default:
                UE_LOG(LogTemp, Warning, TEXT("Unsupported spawn style"));
                break;
        }
    });

    // Start the timer using the initial callback
    GetWorld()->GetTimerManager().SetTimer(SpawnTimer, SequentialSpawnStartCallback, TimeBetweenSpawns, false);
}

void AYourClass::SequentialSpawn()
{
    if (SequentialIndex < SelectedPointIndexList.Num())
    {
        SelectedPointIndex = SelectedPointIndexList[SequentialIndex];
        SpawnEnemy();
        SequentialIndex++;

        // Set a timer to call this function again after a delay
        FTimerDelegate SequentialSpawnCallback;
        SequentialSpawnCallback.BindLambda([this]()
        {
            SequentialSpawn();
        });

        GetWorld()->GetTimerManager().SetTimer(SequentialTimer, SequentialSpawnCallback, DelayBetweenSequentialSpawns, false);
    }
    else
    {
        // All spawns are done, wait for the next cycle
        StartSequentialSpawn();
    }
}

void AYourClass::ReverseSequentialSpawn()
{
    if (ReverseIndex >= 0)
    {
        SelectedPointIndex = SelectedPointIndexList[ReverseIndex];
        SpawnEnemy();
        ReverseIndex--;

        // Set a timer to call this function again after a delay
        FTimerDelegate ReverseSpawnCallback;
        ReverseSpawnCallback.BindLambda([this]()
        {
            ReverseSequentialSpawn();
        });

        GetWorld()->GetTimerManager().SetTimer(SequentialTimer, ReverseSpawnCallback, DelayBetweenSequentialSpawns, false);
    }
    else
    {
        // All spawns are done, wait for the next cycle
        StartSequentialSpawn();
    }
}

void AYourClass::MiddleOutSpawn()
{
    int32 MidIndex = SelectedPointIndexList.Num() / 2;
    int32 LeftIndex = MidIndex - 1;
    int32 RightIndex = MidIndex;

    auto MiddleOutCallback = [this, LeftIndex, RightIndex]() mutable
    {
        if (RightIndex < SelectedPointIndexList.Num())
        {
            SelectedPointIndex = SelectedPointIndexList[RightIndex];
            SpawnEnemy();
            RightIndex++;
        }

        if (LeftIndex >= 0)
        {
            SelectedPointIndex = SelectedPointIndexList[LeftIndex];
            SpawnEnemy();
            LeftIndex--;
        }

        // Set a timer to call this function again after a delay
        FTimerDelegate MiddleOutSpawnCallback;
        MiddleOutSpawnCallback.BindLambda([this, LeftIndex, RightIndex]() mutable
        {
            MiddleOutSpawn();
        });

        GetWorld()->GetTimerManager().SetTimer(SequentialTimer, MiddleOutSpawnCallback, DelayBetweenSequentialSpawns, false);
    };

    // Start the middle-out spawn sequence
    MiddleOutCallback();
}

이 예제에서는 mutable 키워드를 사용하여 MiddleOutCallback 람다 함수 내에서 LeftIndex와 RightIndex 변수를 수정할 수 있도록 했습니다. FTimerDelegate와 BindLambda를 사용하여 언리얼 스타일로 람다 함수를 정의하고 타이머 콜백을 설정했습니다.