쓰는 곳에서 바로 작성하는 함수
이전 페이지에서는 오버로딩 덕분에 여러 함수가 같은 이름을 공유할 수 있다는 것을 보았습니다. 하지만 때로는 이름 있는 함수가 전혀 필요 없을 때도 있습니다. 아주 작은 로직 한 조각이 딱 한 번, 쓰는 바로 그 자리에서 필요한데, 거기에 이름을 붙이면 오히려 어수선해질 뿐인 경우죠. 그것이 바로 람다입니다. 인라인으로 정의할 수 있는 익명 함수입니다.
람다에는 네 부분으로 이루어진 독특한 형태가 있습니다:
[capture](parameters) -> return_type { body }
[]는 지금 보고 있는 것이 람다라는 것을 알려주는 표시입니다. 반환 타입은 선택 사항이며, 보통 컴파일러가 추론합니다. 가능한 가장 단순한 예는 다음과 같습니다:
greet는 그저 하나의 변수(타입을 말로 적을 수 없어서 auto에 저장합니다)이며 ()로 호출할 수 있습니다. 매개변수가 있는 람다도 일반 함수와 똑같이 동작합니다:
캡처: 주변 스코프에 손을 뻗다
람다를 단순한 이름 없는 함수 이상으로 만들어 주는 부분이 바로 캡처 목록, 즉 []입니다. 이것은 람다가 자신의 매개변수뿐 아니라, 자신이 정의된 스코프의 변수들도 사용할 수 있게 해 줍니다.
[x]로 값에 의한 캡처: 람다는 자신만의 복사본을 얻으며, 그 복사본은 람다가 만들어지는 순간에 고정됩니다.
scale(5)가 50을 출력했다는 점에 주목하세요. 람다가 만들어졌을 때 존재하던 factor 값 10을 사용한 것입니다. 값에 의한 캡처는 스냅샷을 찍습니다.
[&x]로 참조에 의한 캡처: 람다는 원래 변수를 참조하며, 이후의 변경을 볼 수 있고 그것을 수정할 수도 있습니다.
람다가 사용하는 모든 것을 [=](전부 값으로) 또는 [&](전부 참조로)로 캡처할 수도 있습니다. 편리하지만, 명시적으로 적는 것([total] 또는 [&total])이 람다가 정확히 무엇을 건드리는지 문서화해 주고, 따라 읽고 추론하기도 더 쉽습니다.
댕글링 참조 함정
참조에 의한 캡처는 강력한 만큼 위험하기도 합니다. 참조는 원래 변수가 살아 있는 동안에만 유효합니다. 람다가 자신이 캡처한 대상보다 오래 살아남으면 댕글링 참조와 **정의되지 않은 동작(undefined behavior)**이 발생합니다. 프로그램이 크래시할 수도, 쓰레기 값을 출력할 수도, 우연히 잘 동작하는 것처럼 보일 수도 있습니다.
이것이 전형적인 실수입니다: 지역 변수를 참조로 캡처하는 람다를 반환하는 것.
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // 버그: count는 여기서 소멸한다
}
// 반환된 람다는 이제 파괴된 메모리를 참조하고 있다.
makeCounter가 반환되면 지역 변수 count는 파괴되지만, 람다는 여전히 그것에 대한 참조를 들고 있습니다. 반환된 람다를 호출하면 죽은 메모리를 건드리게 됩니다. 해결책은 값으로 캡처해서 람다가 자신의 상태를 직접 소유하게 하는 것입니다:
경험적 규칙: 람다가 즉시 지역적으로 사용될 때만(아래 알고리즘처럼) 참조로 캡처하세요. 람다가 저장되거나, 반환되거나, 나중에 실행되는 순간부터는 값에 의한 캡처를 선호하세요.
mutable과 반환 타입
방금 그 예제에서 mutable을 알아채셨나요? 기본적으로 값에 의한 캡처는 람다 안에서 **const**입니다. 복사본을 읽을 수는 있어도 바꿀 수는 없죠. mutable을 붙이면 람다가 호출과 호출 사이에 자신의 복사본을 수정할 수 있게 됩니다.
mutable은 람다의 비공개 복사본에만 영향을 줍니다. 바깥의 seen은 그대로 유지되는데, 바로 그것이 값에 의한 캡처의 핵심입니다.
대부분의 경우 컴파일러는 반환 타입을 잘 추론합니다. ->로 직접 명시해야 하는 경우는, 분기마다 서로 다른 타입을 반환할 수 있는 람다처럼 모호함이 있을 때뿐입니다:
// -> 가 없으면 컴파일러는 int와 double 사이에서 결정하지 못한다
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
람다와 알고리즘: 진짜 보상
람다가 C++에 추가된 이유는 표준 라이브러리 알고리즘에 짧은 로직 조각을 넘겨주기 위해서입니다. 람다가 나오기 전에는 별도의 이름 있는 함수나, 쓰이는 곳과 동떨어진 곳에 둔 번거로운 함수 객체를 작성해야 했습니다. 이제는 로직이 호출 지점 바로 옆에 자리합니다.
가장 흔한 예는 사용자 정의 정렬 순서입니다:
캡처는 여기서 진가를 발휘합니다. 람다가 필터링이나 집계의 기준이 될 값을 끌어올 수 있기 때문입니다. 다음은 사용자가 정한 기준값을 넘는 숫자가 몇 개인지 셉니다:
이 람다들은 즉시 사용되고 주변 함수보다 오래 살아남지 않기 때문에, 여기서는 참조로 캡처해도([&passMark]) 안전합니다. 다만 값에 의한 캡처도 똑같이 명확하면서 결코 댕글링되지 않습니다.
다음: 포인터
람다는 더 깊은 질문을 슬며시 던졌습니다. [&x]를 캡처할 때, 람다는 x의 위치 를 붙잡고 있으며, 그 위치는 x가 살아 있는 동안에만 유효합니다. 무언가가 메모리의 어디에 존재하는지를 가리키는 값, 그리고 그것이 가리키던 대상이 사라지면 무슨 일이 벌어지는지 — 이 개념이 바로 다음 페이지의 주제입니다. 우리는 포인터를 정면으로 마주합니다. 주소를 얻는 법, 그것을 따라가는 법, 그리고 방금 본 것과 똑같은 댕글링 문제가 C++ 전반에 어떻게 나타나는지를 배웁니다.
자주 묻는 질문
C++에서 람다란 무엇인가요?
람다는 사용하는 바로 그 자리에서 인라인으로 작성할 수 있는 익명 함수입니다. 문법은 [captures](parameters){ body }입니다. std::sort에 넘기는 비교 함수처럼 짧고 한 번만 쓰는 동작에 안성맞춤이며, 다른 곳에 별도의 이름 있는 함수를 선언할 필요가 없습니다.
C++ 람다에서 값에 의한 캡처와 참조에 의한 캡처의 차이는 무엇인가요?
[x]는 람다가 만들어지는 순간에 고정된 x의 복사본을 캡처합니다. [&x]는 원래 x에 대한 참조를 캡처하므로, 람다는 이후의 변경을 볼 수 있고 그 값을 수정할 수도 있습니다. [&]는 캡처한 변수가 람다보다 오래 살아 있다고 보장될 때만 사용하세요. 그렇지 않으면 댕글링 참조가 생깁니다.
C++ 람다가 캡처한 변수를 수정할 수 없다고 하는 이유는 무엇인가요?
값에 의한 캡처는 기본적으로 람다 안에서 const입니다. mutable 키워드를 추가하면([x]() mutable { x++; }) 람다가 자신의 복사본을 변경할 수 있습니다. 단, 이는 람다의 복사본만 바꿀 뿐, 바깥에 있는 원래 변수는 바뀌지 않는다는 점에 유의하세요.