C++ 프로그래밍

C++ 람다 사용시 변수 캡처 주의점과 shared_ptr의 활용

하늘흐늘 2022. 1. 21. 20:20
반응형

C++의 람다는 근래에 많이 쓰이는 기술 중에 하나 입니다. 람다는 흔희 변수를 캡처하여 내부에서 많이 사용합니다. 이럴 때 생각해야 할 점이 변수를 참조로 캡처할 것인가 복사로 캡처할 것인가 하는 점입니다. 기본적으로 내부에서 캡처한 외부 변수의 값을 변경하고자 하거나 복사에 따른 오버헤드가 크다고 생각한다면 혹은 객체 내부에서 객체 함수 등을 참조해야 한다면 참조로 캡처를 사용하게 됩니다. 반대로 내부에서만 값을 변경 또는 사용하거나 복사에 따른 오버헤드가 크지 않으면 복사로 캡처하여 사용합니다.

람다는 참조로 캡처하여 사용할 때 주의점은 캡처한 변수가 람다를 다 사용할 때까지 메모리에 남아 있어야 한다는 것입니다.

간단한 아래와 같은 예를 하나 살펴보겠습니다.
참고로 이 예제는 실제로 일어날 수 있는 상황을 간단한 예시 코드로 바꾸어 놓은 것이라 조금 어색하게 느껴질 수도 있습니다. 하지만 코드를 이해하게 되면 나중에 비슷한 케이스의 코드 작성시 도움을 받을 수 있을 것입니다.

int* test = new int;
*test = 10;

auto fun1 = [&]() {
	(*test)++;

	cout << *test << endl;
};

auto fun2 = [&]() {
	(*test)++;

	cout << *test << endl;
};

cout << *test << endl;

fun1();
fun2();

delete test;

결과 값은 아래와 같이 나옵니다.

10
11
12

위의 예제는 람다 내부에서 사용하는 참조로 캡처한 변수가 람다가 사용하는 동안에 메모리 상에 존재하므로 이상없이 작동합니다. 그런데 만약 코드가 아래와 같이 바뀌는 상황을 한번 생각해 보겠습니다.

int* test = new int;
*test = 10;

auto fun1 = [&]() {
	(*test)++;

	cout << *test << endl;
};

auto fun2 = [&]() {
	(*test)++;

	cout << *test << endl;
};

cout << *test << endl;

fun1();

delete test;

fun2();

위와 같은 경우에는 fun2에서 참조로 캡처한 test가 메모리 상에서 삭제되는 관계로 프로그램이 비정상 종료되게 됩니다. 람다에서 참조로 캡처할 때 주의점은 참조로 캡처한 변수가 메모리상에 존재하고 있어야 한다는 것입니다. 스택에 있는 변수 같은 것을 참조로 캡처할 때는 특히나 주의해야 합니다.

만약 이와 같은 경우에 예외 상황의 예에서 delete test의 위치를 바꾸지 않고 어떻게 처리할 수 있을까여?

한가지 드는 생각은 참조로 캡처할 때 문제가 생기니 복사로 캡처하는 것을 생각해 볼 수 있습니다.

복사로 캡처한 아래 예를 살펴보겠습니다.

int* test = new int;
*test = 10;

int a = 1;

auto fun1 = [test2 = *test]() mutable {
	test2++;

	cout << test2 << endl;
};

auto fun2 = [test2 = *test] () mutable {
	test2++;

	cout << test2 << endl;
};


cout << *test << endl;

fun1();

delete test;

fun2();

결과값은 아래와 같습니다.

10
11
11


결과값을 설명하기 전에 우선 mutable을 사용한 이유 부터 설명하겠습니다. 람다에서 복사 캡처시 주의해야 할 부분은 복사 값은 const로만 복사되기 때문에 값을 수정할 수 없다는 것입니다. 이럴 때 복사한 값을 수정하려면 mutable을 람다 함수에 붙여주어 복사한 값을 수정할 수 있게 만들어야 합니다.
다음으로 결과 값을 이야기 하자면 값을 복사한 것이기 때문에 복사로 캡처한 변수들과 원본 변수들은 모두 별도 메모리를 가집니다. 그런 관계로 값의 변화가 서로에게 영향을 주지 않습니다. 이런 이유로 프로그램은 정상적으로 실행되지만 원하는 결과값인 10, 11, 12가 나오지는 않습니다. 물론 10, 11, 11이 원하는 의도의 결과 값이었다면 여기서의 람다 사용은 옳은 것이 될 수도 있습니다.

여기까지 간단한 예를 통하여 람다에서 참조 캡처와 복사 캡처를 사용시 주의해야할 사항을 살펴보았습니다. 그럼 앞의 예와 같은 예외 상황에서 delete test의 위치를 바꾸지 않고 어떻게 처리할 수 있을까여? 쉽게 생각해서 메모리 할당과 해제를 고려해서 람다를 사용해야하는 것을 우선 고려해야 합니다. 다음으로 shared_ptr를 응용하여 이런 경우에 어디서 참조로 캡처된 변수가 사용되어도 캡처된 변수가 메모리에 존재하는 것을 보장받을 수 있습니다.

아래 예를 살펴봅시다.

shared_ptr<int> test = make_shared<int>(10);

auto fun1 = [&]()  {
	(*test)++;

	cout << *test << endl;
};

auto fun2 = [&] ()  {
	(*test)++;

	cout << *test << endl;
};

cout << *test << endl;

fun1();
fun2();

결과값은 아래와 같습니다.

10
11
12

원하는 결과가 나왔고 메모리 어딘가에서 객체가 삭제되어도 완전히 삭제되는 것은 shared_ptr의 모든 참조가 해제되는 순간까지 보장받습니다.

여기까지 람다의 참조 및 복사 캡처시 주의점과 shared_ptr을 응용하는 방법을 살펴보았습니다.

 

반응형