콘텐츠로 이동

람다 표현식 (Lambda Expressions)

적절한 경우 람다 식을 사용합니다. 람다가 현재 범위를 벗어나는 경우 명시적 캡처를 선호합니다.

람다 표현식은 익명 함수 객체를 생성하는 간결한 방법입니다. 함수를 인수로 전달할 때 종종 유용합니다. 예를 들어:

std::sort(v.begin(), v.end(), [](int x, int y) {
  return Weight(x) < Weight(y);
});

또한 명시적으로 이름으로 또는 기본 캡처를 사용하여 암시적으로 바깥쪽 범위에서 변수를 캡처할 수 있습니다. 명시적 캡처를 사용하려면 각 변수를 값 또는 참조 캡처로 나열해야 합니다.

int weight = 3;
int sum = 0;
// Captures `weight` by value and `sum` by reference.
std::for_each(v.begin(), v.end(), [weight, &sum](int x) {
  sum += weight * x;
});

기본 캡처는 멤버가 사용되는 경우를 포함하여 람다 본문에서 참조되는 모든 변수를 암시적으로 캡처합니다.

const std::vector<int> lookup_table = ...;
std::vector<int> indices = ...;
// Captures `lookup_table` by reference, sorts `indices` by the value
// of the associated element in `lookup_table`.
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
  return lookup_table[a] < lookup_table[b];
});

변수 캡처에는 이동 전용 변수를 값으로 캡처하거나 일반 참조 또는 값 캡처로 처리되지 않는 기타 상황에 사용할 수 있는 명시적 초기화 프로그램도 있을 수 있습니다.

std::unique_ptr<Foo> foo = ...;
[foo = std::move(foo)] () {
  ...
}

이러한 캡처(종종 "초기 캡처" 또는 "일반화된 람다 캡처"라고도 함)는 포함하는 범위에서 실제로 아무것도 "캡처"할 필요가 없거나 포함하는 범위의 이름을 가질 필요도 없습니다. 이 구문은 람다 객체의 멤버를 정의하는 완전히 일반적인 방법입니다.

[foo = std::vector<int>({1, 2, 3})] () {
  ...
}

이니셜라이저를 사용한 캡처 유형은 auto 와 동일한 규칙을 사용하여 추론됩니다.

  • 람다는 STL 알고리즘에 전달될 함수 객체를 정의하는 다른 방법보다 훨씬 더 간결하므로 가독성이 향상될 수 있습니다.
  • 기본 캡처를 적절하게 사용하면 중복성을 제거하고 기본값에서 중요한 예외를 강조 표시할 수 있습니다.
  • Lambdas, std::function 및 std::bind는 범용 콜백 메커니즘으로 조합하여 사용할 수 있습니다. 바인딩된 함수를 인수로 사용하는 함수를 쉽게 작성할 수 있습니다.

  • 람다의 변수 캡처는 특히 람다가 현재 범위를 벗어나는 경우 매달린 포인터 버그의 원인이 될 수 있습니다.

  • 값에 의한 기본 캡처는 현수 포인터 버그를 방지하지 않기 때문에 오해의 소지가 있을 수 있습니다. 값으로 포인터를 캡처하면 전체 복사가 발생하지 않으므로 참조로 캡처하는 것과 동일한 수명 문제가 발생하는 경우가 많습니다. 값으로 이를 캡처할 때 특히 혼란스럽습니다. 왜냐하면 this의 사용이 종종 암시적이기 때문입니다.
  • 캡처는 실제로 새 변수를 선언하지만(캡처에 이니셜라이저가 있는지 여부에 관계없이) C++의 다른 변수 선언 구문과 전혀 비슷하지 않습니다. 특히, 변수 유형이나 자동 자리 표시자를 위한 공간이 없습니다(비록 init 캡처가 캐스트를 사용하여 간접적으로 나타낼 수 있음에도 불구하고). 이로 인해 이를 선언으로 인식하는 것조차 어려워질 수 있습니다.
  • Init 캡처는 본질적으로 유형 추론에 의존하며 auto와 동일한 많은 단점을 안고 있으며 구문이 독자에게 추론이 발생하고 있다는 신호조차 주지 않는다는 추가 문제가 있습니다.
  • 람다를 사용하면 통제가 불가능할 수 있습니다. 매우 긴 중첩 익명 함수는 코드를 이해하기 어렵게 만들 수 있습니다.

  • 아래에 설명된 대로 형식을 지정하여 적절한 경우 람다 식을 사용합니다.

  • 람다가 현재 범위를 벗어날 수 있는 경우, 참조 기본 캡처([&])보다 변수를 콕 집어 캡처([&foo])하는 명시적 캡처를 선호합니다. 그래야 무엇을 참조로 잡는지 드러나고, 멤버 함수를 캡처하면 컴파일 오류가 나서 위험을 조기에 알 수 있습니다. (구체적인 예시는 아래 "이해하기 쉽게 설명하기" 참조.)
  • 람다의 수명이 잠재적인 캡처보다 확실히 짧은 경우에만 참조에 의한 기본 캡처( [&] )를 사용하세요.
  • 값별 기본 캡처( [=] )는 짧은 람다에 대한 몇 가지 변수를 바인딩하는 수단으로만 사용합니다. 여기서 캡처된 변수 집합은 한 눈에 명확하고 이를 암시적으로 캡처하지 않습니다. (즉, 비정적 클래스 멤버 함수에 나타나고 본문의 비정적 클래스 멤버를 참조하는 람다는 명시적으로 또는 [&] 를 통해 이를 캡처해야 합니다.) 값으로 기본 캡처를 사용하여 길거나 복잡한 람다를 작성하지 않는 것이 좋습니다.
  • 둘러싸는 범위에서 실제로 변수를 캡처하는 경우에만 캡처를 사용하십시오. 새 이름을 소개하거나 기존 이름의 의미를 크게 변경하기 위해 이니셜라이저와 함께 캡처를 사용하지 마세요. 대신, 기존 방식으로 새 변수를 선언한 다음 이를 캡처하거나, 람다 약칭을 피하고 함수 개체를 명시적으로 정의하세요.
  • 매개변수 및 반환 유형 지정에 대한 지침은 유형 추론 섹션을 참조하세요.

이해하기 쉽게 설명하기

핵심: 적절할 때 람다, 범위를 벗어나면 명시적 캡처

람다는 익명 함수 객체를 간결하게 만드는 방법으로, STL 알고리즘에 함수를 넘길 때 특히 유용합니다.

std::sort(v.begin(), v.end(), [](int x, int y) {
  return Weight(x) < Weight(y);
});

캡처의 위험: 댕글링 포인터

람다가 현재 범위를 벗어나 나중에 실행될 수 있다면, 참조 캡처는 이미 파괴된 객체를 가리키는 댕글링 포인터 버그가 됩니다.

// 나쁨: [&]가 foo(그리고 아마 this)를 참조 캡처 — 한눈에 안 보임
executor->Schedule([&] { Frobnicate(foo); });

// 더 나음: [&foo]로 명시 — 의도가 드러나고, 멤버 함수면 컴파일도 실패해 위험을 알림
executor->Schedule([&foo] { Frobnicate(foo); });

지침

  • [&](참조 기본 캡처): 람다의 수명이 캡처 대상보다 확실히 짧을 때만.
  • [=](값 기본 캡처): 짧은 람다에서 명확한 소수 변수만 묶을 때. 포인터나 this를 값 캡처해도 수명 문제는 그대로 남으니 주의.
  • 길거나 복잡한 람다에는 기본 캡처를 쓰지 말고, 변수를 명시적으로 나열하세요.
  • init 캡처([foo = std::move(foo)])는 실제로 바깥에서 무언가를 캡처할 때만. 새 이름을 만들거나 기존 이름의 의미를 바꾸는 용도로 쓰지 마세요.
  • 매개변수·반환 타입 지정은 "타입 추론" 섹션을 참조하세요.