람다 표현식 (Lambda Expressions)¶
적절한 곳에 람다 표현식을 사용하세요. 람다가 현재 범위를 벗어날 수 있다면 명시적 캡처를 선호하세요.
정의:
람다 표현식은 익명 함수 객체를 만드는 간결한 방법입니다. 함수를 인수로 전달할 때 특히 유용합니다. 예를 들면 다음과 같습니다.
또한 람다는 바깥 범위의 변수를 이름으로 명시해서, 또는 기본 캡처를 써서 암시적으로 캡처할 수 있습니다. 명시적 캡처는 각 변수를 값 캡처인지 참조 캡처인지와 함께 하나씩 나열해야 합니다.
int weight = 3;
int sum = 0;
// `weight`는 값으로, `sum`은 참조로 캡처합니다.
std::for_each(v.begin(), v.end(), [weight, &sum](int x) {
sum += weight * x;
});
기본 캡처는 람다 본문에서 참조하는 모든 변수를 암시적으로 캡처하며, 멤버를 사용한다면 this도 캡처합니다.
const std::vector<int> lookup_table = ...;
std::vector<int> indices = ...;
// `lookup_table`을 참조로 캡처하고, `lookup_table`의 대응하는 원소 값을
// 기준으로 `indices`를 정렬합니다.
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
return lookup_table[a] < lookup_table[b];
});
변수 캡처에는 명시적인 초기화식을 붙일 수도 있습니다. 이동 전용 변수를 값으로 캡처하거나, 일반적인 참조·값 캡처로는 처리되지 않는 상황에 쓸 수 있습니다.
이런 캡처(흔히 "init 캡처" 또는 "일반화된 람다 캡처"라고 부릅니다)는 바깥 범위에서 실제로 무언가를 "캡처"할 필요도, 바깥 범위에 이름이 있을 필요도 없습니다. 이 문법은 람다 객체의 멤버를 정의하는 완전히 일반적인 방법입니다.
초기화식이 있는 캡처의 타입은 auto와 같은 규칙으로 추론됩니다.
장점:
- 람다는 STL 알고리즘에 넘길 함수 객체를 정의하는 다른 방법들보다 훨씬 간결하며, 그만큼 가독성이 좋아질 수 있습니다.
- 기본 캡처를 적절히 쓰면 중복을 없애고 기본값에서 벗어나는 중요한 예외를 드러낼 수 있습니다.
- 람다,
std::function,std::bind를 조합하면 범용 콜백 메커니즘으로 쓸 수 있습니다. 바인딩된 함수를 인수로 받는 함수를 쉽게 작성할 수 있습니다.
단점:
- 람다의 변수 캡처는 댕글링 포인터 버그의 원인이 될 수 있으며, 람다가 현재 범위를 벗어날 때 특히 그렇습니다.
- 값에 의한 기본 캡처는 댕글링 포인터 버그를 막아 주지 않기 때문에 오해를 부를 수 있습니다. 포인터를 값으로 캡처해도 깊은 복사가 일어나지 않으므로, 참조 캡처와 같은 수명 문제가 그대로 생기는 경우가 많습니다.
this를 값으로 캡처할 때 특히 혼란스러운데,this의 사용이 암시적인 경우가 많기 때문입니다. - 캡처는 (초기화식이 있든 없든) 실제로는 새 변수를 선언하는 것이지만, 그 모습이 C++의 다른 어떤 변수 선언 문법과도 닮지 않았습니다. 특히 변수의 타입을 적을 자리도, 심지어
auto자리표시자를 적을 자리도 없습니다(init 캡처는 캐스트 같은 방법으로 간접적으로 나타낼 수 있기는 합니다). 그래서 그것이 선언이라는 사실조차 알아보기 어려울 수 있습니다. - init 캡처는 본질적으로 타입 추론에 의존하므로
auto가 가진 단점을 상당 부분 그대로 안고 있으며, 문법이 독자에게 추론이 일어나고 있다는 신호조차 주지 않는다는 문제가 더해집니다. - 람다 사용이 걷잡을 수 없어질 수 있습니다. 아주 길고 중첩된 익명 함수는 코드를 이해하기 어렵게 만듭니다.
결정:
- 적절한 곳에 람다 표현식을 사용하되, 형식은 아래에 설명된 대로 맞추세요.
-
람다가 현재 범위를 벗어날 수 있다면 명시적 캡처를 선호하세요. 예를 들어 다음과 같이 쓰는 대신,
{ Foo foo; ... executor->Schedule([&] { Frobnicate(foo); }) ... } // 나쁨! 이 람다가 `foo`에 대한 참조를 사용한다는 사실과, (`Frobnicate`가 // 멤버 함수라면) `this`까지 사용할 수 있다는 사실이 훑어봐서는 드러나지 // 않습니다. 함수가 반환된 뒤에 람다가 호출된다면 `foo`도 바깥 객체도 // 이미 파괴되었을 수 있으므로 좋지 않습니다.다음과 같이 쓰는 편이 낫습니다.
-
참조에 의한 기본 캡처(
[&])는 람다의 수명이 캡처 대상 어느 것보다도 명백히 짧을 때만 사용하세요. - 값에 의한 기본 캡처(
[=])는 짧은 람다에서 변수 몇 개를 묶는 수단으로만 사용하세요. 캡처되는 변수 집합이 한눈에 분명하고,this가 암시적으로 캡처되지 않는 경우여야 합니다. (즉, 비정적 클래스 멤버 함수 안에 있으면서 본문에서 비정적 클래스 멤버를 참조하는 람다는this를 명시적으로 캡처하거나[&]를 통해 캡처해야 합니다.) 값에 의한 기본 캡처로 길거나 복잡한 람다를 작성하지 않는 편이 좋습니다. - 캡처는 바깥 범위에서 실제로 변수를 캡처할 때만 사용하세요. 새 이름을 도입하거나 기존 이름의 의미를 크게 바꾸려고 초기화식이 있는 캡처를 쓰지 마세요. 그런 경우에는 관례적인 방식으로 새 변수를 선언한 다음 그것을 캡처하거나, 람다라는 축약 표기를 포기하고 함수 객체를 명시적으로 정의하세요.
- 매개변수와 반환 타입을 지정하는 지침은 타입 추론 절을 참조하세요.
옮긴이 풀이¶
핵심: 적절할 때 람다, 범위를 벗어나면 명시적 캡처¶
람다는 익명 함수 객체를 간결하게 만드는 방법으로, STL 알고리즘에 함수를 넘길 때 특히 유용합니다.
캡처의 위험: 댕글링 포인터¶
람다가 현재 범위를 벗어나 나중에 실행될 수 있다면, 참조 캡처는 이미 파괴된 객체를 가리키는 댕글링 포인터 버그가 됩니다.
값 캡처라고 안전한 것도 아닙니다. 포인터를 값으로 캡처해도 가리키는 대상이 복사되지는 않으므로 수명 문제는 그대로입니다. this를 값으로 캡처하는 경우가 특히 헷갈리는데, 멤버를 쓰기만 해도 this가 암시적으로 캡처되기 때문입니다.
지침¶
[&](참조 기본 캡처): 람다의 수명이 캡처 대상보다 확실히 짧을 때만.[=](값 기본 캡처): 짧은 람다에서 명확한 소수 변수만 묶을 때.- 길거나 복잡한 람다에는 기본 캡처를 쓰지 말고, 변수를 명시적으로 나열하세요.
- init 캡처(
[foo = std::move(foo)])는 실제로 바깥에서 무언가를 캡처할 때만. 새 이름을 만들거나 기존 이름의 의미를 바꾸는 용도로 쓰지 마세요. - 매개변수·반환 타입 지정은 타입 추론 절을 참조하세요.