[Advanced C++] 55. lambda capture, lambda 복사, std::reference_wrapper, std::ref()

dev.kelvin·2025년 5월 26일
1

Advanced C++

목록 보기
55/74
post-thumbnail

1. lambda capture

람다 캡처

다음 코드를 확인해보자

	std::array<std::string_view, 3> colors = { "Red", "Green", "Blue" };

	std::string searchColor = "Green";

	auto found{ std::find_if(colors.begin(), colors.end(), [](std::string_view str) {
		return str.find(searchColor) == std::string_view::npos;
	}) };

위 코드는 컴파일 되지 않는다

일반적인 { }블록은 블록 외부의 모든 데이터에 접근이 가능하지만 lambda는 외부에서 정의된 특정한 데이터에만 접근이 가능하다

여기서 특정 데이터는 static한 수명을 가진 객체 (전역 변수, static local variable), 그리고 constexpr 객체가 될 수 있다

하지만 여기서 searchColor는 그렇지 않기 때문에 lambda에서 외부에 정의된 데이터에 접근이 불가능하다

그렇다면 lambda 내부에서 외부에 정의된 데이터에 접근하려면 어떻게 해야할까? 이럴때 바로 lambda capture를 사용해야 한다

lambda capture절은 lambda가 일반적으로 접근할 수 없는 외부 데이터에 간접적으로 접근 권한을 부여해준다

원하는 데이터를 lambda capture절인 [ ]에 적어주면 된다

	auto found{ std::find_if(colors.begin(), colors.end(), [searchColor](std::string_view str) {
		return str.find(searchColor) == std::string_view::npos;
	}) };

이렇게 캡처한 데이터에 lambda가 직접적으로 접근하는것 처럼 보이지만 사실 그렇지 않다

캡처된 데이터들의 복사본이 lambda 내부에 동일한 이름으로 만들어진다, 이렇게 만들어진 데이터 복사본은 lambda 외부에 정의된 데이터 값으로 초기화 된다

따라서 위 코드는 lambda 외부의 searchColor값으로 초기화 된 searchColor라는 이름의 복사본이 생성된 것이고 이 복사본 변수에 lambda가 접근하는 것이다

이때 이 복사본은 이름은 같지만 타입은 원래 변수와 반드시 같은 타입을 갖지는 않는다

이전에 정리했듯 lambda는 함수처럼 보이지만 사실상 함수처럼 호출할 수 있는 객체이다 (functor)

컴파일러가 lambda 정의를 만나게 되면 lambda에 대한 사용자 정의 객체의 정의를 생성한다, 이때 캡처된 데이터는 해당 객체의 멤버가 된다

런타임에 이러한 lambda 정의가 발생하면 lambda객체가 인스턴스화 되고 lambda의 멤버가 초기화 된다

lambda의 캡처된 변수들은 기본적으로 const로 처리된다 (operator()가 캡처된 변수들을 const로 취급한다)
따라서 캡처된 변수들은 수정이 불가능하다

	auto found{ std::find_if(colors.begin(), colors.end(), [searchColor](std::string_view str) {
		searchColor = "Red"; //compile error
		return str.find(searchColor) == std::string_view::npos;
	}) };

만약 캡처된 변수의 수정을 허용하려면 lambda를 mutable로 처리해야 한다

	//mutable
	auto found{ std::find_if(colors.begin(), colors.end(), [searchColor](std::string_view str) mutable {
		searchColor = "Red"; //ok
		return str.find(searchColor) == std::string_view::npos;
	}) };

이때 lambda의 캡처 데이터는 복사본이기 때문에 수정을 해도 원본 데이터는 변경되지 않는다, 또한 캡처된 변수는 lambda 객체의 멤버이기 때문에 해당 lambda를 여러번 호출해도 멤버 값이 유지가 된다

mutable lambda는 별로 권장하지 않는다

참조에 의한 캡처

함수의 매개변수를 참조로 전달하여 원본값을 변경할 수 있는것 처럼 lambda도 참조를 사용하여 캡처된 변수의 원본값 수정이 가능하다

이때 캡처된 변수에 &를 붙이게 되면 해당 캡처 변수는 non-const가 된다

	//&캡처
	auto found{ std::find_if(colors.begin(), colors.end(), [&searchColor](std::string_view str) {
		searchColor = "Red"; //참조로 캡처 변수를 사용했기 때문에 수정이 가능하다
		return str.find(searchColor) == std::string_view::npos;
	}) };

이제 캡처된 변수(복사본)뿐 아니라 원본값 자체가 변경이 된다

lambda에서 ,를 이용하여 여러개의 변수를 캡처할 수 있다 이때 &와 일반 값 캡처 변수가 섞일 수 있다

	std::string searchColor = "Green";
	int temp{};
    
	auto found{ std::find_if(colors.begin(), colors.end(), [&searchColor, temp](std::string_view str) {
		searchColor = "Red";
		std::cout << temp;
		return str.find(searchColor) == std::string_view::npos;
	}) };

searchColor는 &로 temp는 그냥 값으로 캡처한 코드이다

default capture

프로그래머가 명시적으로 캡처하려는 데이터를 나열하는건 번거로울 수 있고 lambda 수정 시 해당 캡처 변수를 추가하거나 제거를 깜빡할 수 있다

이를 위해 컴파일러는 캡처해야 할 변수 목록을 자동으로 생성할 수 있다

기본 캡처는 lambda에서 사용된 모든 변수를 캡처한다, 사용되지 않는 변수는 캡처하지 않는다

이때 사용된 모든 변수를 값으로 캡처하려면 캡처 값으로 =을 사용하고 참조로 캡처하려면 &를 사용하면 된다

	std::array grade{ 70, 20, 60, 30, 100 };

	int a{ 10 };
	int b{ 7 };
	
    //값 캡처
	auto found{ std::find_if(grade.begin(), grade.end(), [=](int gradeValue) {
		return a * b == gradeValue;
	}) };

	//참조 캡처
	auto found{ std::find_if(grade.begin(), grade.end(), [&](int gradeValue) {
		a = 20;
		return a * b == gradeValue;
	}) };

이때 기본 캡처는 명시적 캡처와 같이 사용이 가능하다

	auto found{ std::find_if(grade.begin(), grade.end(), [=, &b](int gradeValue) {
		a = 20; //error
		b = 10; //ok
		return a * b == gradeValue;
	}) };

b는 명시적으로 참조로 캡처하고 =을 이용하여 나머지를 값으로 캡처한다, 따라서 a는 수정이 불가능하지만 b는 수정이 가능해진다

lambda 캡처에서 변수 정의

lambda의 캡처에서 해당 lambda 범위에서만 사용 가능한 변수를 정의할 수 있다 (타입 지정X)

	std::array grade{ 70, 20, 60, 30, 100 };

	int a{ 10 };
	int b{ 7 };

	auto found{ std::find_if(grade.begin(), grade.end(), [testValue{a * b}](int gradeValue) {
		return testValue == gradeValue;
	}) };

여기서 testValue는 lambda의 캡처절에서 정의되었고 타입은 자동으로 int로 추론된다

이렇게 캡처절에서 정의된 변수는 lambda가 정의될 때 한번만 계산된다, 따라서 이후에 호출할 때는 이미 정의된 변수를 계속해서 사용하는것이다, 또한 mutable lambda에서 해당 변수를 수정하면 값이 변경된다

	std::array grade{ 70, 20, 60, 30, 100 };

	int a{ 10 };
	int b{ 7 };

	auto found{ std::find_if(grade.begin(), grade.end(), [testValue{a * b}](int gradeValue) mutable {
		testValue = 200;
		return testValue == gradeValue;
	}) };

변수가 굉장히 간단하고 타입이 명확한 경우에만 캡처에서 변수를 정의하는게 좋다, 그렇지 않다면 lambda 외부에서 변수 정의 후 캡처해서 사용하는걸 권장한다

dangling captured variable

변수는 lambda가 정의되는 시점에 캡처된다, 이때 참조로 변수를 캡처했는데 lambda가 정의되기 전에 해당 변수가 소멸된다면 lambda는 dangling 참조를 갖게 된다

    auto Foo(const std::string& color)
    {
        return [&]() { //참조로 캡처
            std::cout << color << '\n';
        };
    }

    int main() 
    {
        auto newColor{ Foo("Red") }; //Red라는 std::string 임시 객체가 생성되고 Foo()의 매개변수로 전달된다, 이때 임시객체이기 때문에 해당 line 이후에 소멸된다

        newColor(); //이미 소멸된 Red라는 std::string 임시객체를 참조하는 캡처를 가진 lambda가 호출되어 dangling 참조가 발생한다 (의도치 않은 동작이 발생할 수 있다)

        return 0;
    }

이때 Foo()에 "Red"가 &가 아닌 값으로 전달되어도 마찬가지이다 (color 매개변수는 해당 함수가 끝나고 소멸되기 때문)

변수를 &로 캡처할 때 항상 lambda보다 캡처된 변수가 더 오래 유효해야 한다

위의 예시에서 color를 계속 유지하고 싶다면 &가 아닌 값으로 캡처해야 한다

lambda 복사

lambda는 함수가 아닌 객체이기 때문에 복사될 수 있다

	int i{};
	auto count{ [i]() mutable {
		std::cout << ++i << '\n';
	}};

	count();

	auto copyCount{ count };

	count();
	copyCount();

위 결과는 1, 2, 2가 된다, count lambda가 복사된 시점에서 i는 1이었기 때문이다
(lambda객체의 복사본이기 때문에 별개의 캡처 변수 복사본을 가지게 된다)

그렇다면 다음 코드는 어떨까?

	void Foo(const std::function<void()>& fn)
    {
        fn();
    }

    int main() 
    {
        int i{};
        auto count{ [i]() mutable {
            std::cout << ++i << '\n';
        }};

        Foo(count);
        Foo(count);
        Foo(count);

        return 0;
    }

결과는 1, 1, 1이 나오게 된다

컴파일러가 Foo(count);를 호출할 때 lambda타입과 std::function<void()>가 일치하지 않는걸 확인하고 lambda를 std::function으로 암시적 변환을 한다, 이 과정에서 lambda의 사본이 생성되게 된다

따라서 Foo()에서 fn()은 실제 전달된 lambda가 호출되는것이 아닌 전달된 lambda의 복사본이 호출되는것이다

그렇기 때문에 각각의 캡처 변수를 가지고 있어 1, 1, 1이 나오게 된다

그렇다면 std::function 매개변수에 전달하면서 복사본을 만들지 않으려면 어떻게 해야할까? lambda를 바로 std::function에 할당하면 된다

    void Foo(const std::function<void()>& fn)
    {
        fn();
    }

    int main() 
    {
        int i{};
        //lambda를 auto가 아닌 std::function타입에 할당함
        std::function count{ [i]() mutable {
            std::cout << ++i << '\n';
        }};

        Foo(count); //1
        Foo(count); //2
        Foo(count); //3

        return 0;
    }

또 다른 방법은 reference wrapper를 사용하는 것이다

std::ref()로 std::reference_wrapper를 만들 수 있다, 이렇게 std::reference_wrapper로 감싸게 되면 값 타입을 참조인 것 처럼 전달이 가능하다

따라서 lambda의 사본이 생성되지 않는다

	#include <functional>

    void Foo(const std::function<void()>& fn)
    {
        fn();
    }

    int main() 
    {
        int i{};
        auto count{ [i]() mutable {
            std::cout << ++i << '\n';
        }};

        Foo(std::ref(count)); //1
        Foo(std::ref(count)); //2
        Foo(std::ref(count)); //3

        return 0;
    }

이렇게 하면 Foo()가 &가 아닌 그냥 값으로 받아도 동일한 결과를 내게 된다 (애초에 std::reference_wrapper로 참조인 것 처럼 전달하기 때문)

profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글