Info

해당 글에서는 C++ lambda의 기본 문법과 capture clause, lambda를 fold expression 안에서 즉시 호출하는 방식에 대해서 다룬다.

Lambda란?


Lambda는 이름이 없는 함수 객체를 간단하게 정의하기 위한 문법이다. 일반적인 형태는 다음과 같다.

[capture](parameters) -> return_type {
    body
}
  • [capture]: lambda 외부의 변수를 어떤 방식으로 사용할지 지정한다.
  • (parameters): 일반 함수와 같은 인자 목록이다.
  • -> return_type: 반환 타입을 지정한다. 생략하면 compiler가 반환 타입을 추론한다.
  • { body }: lambda가 호출되었을 때 실행할 본문이다.

인자가 없다면 (parameters) 부분을 생략할 수 있다.

Lambda의 생략 가능한 문법


다음 세 lambda는 모두 같은 결과를 반환한다.

/* Case 1 */
auto noCapture = [] {
    return 42;
};
 
/* Case 2 */
auto noCapture = []() {
    return 42;
};
 
/* Case 3 */
auto noCapture = []() -> int {
    return 42;
};

인자가 없다면 괄호를 생략할 수 있고, 반환 타입이 명확하다면 -> int도 생략할 수 있다.

여기서 lambda를 저장하는 변수의 타입을 auto로 선언하는 것과 반환 타입을 추론하는 것은 서로 다른 동작이다.

  • auto noCapture: 이름을 직접 작성하기 어려운 lambda의 고유한 closure type을 추론한다.
  • return 42: lambda가 반환하는 값의 타입을 기반으로 반환 타입을 추론한다.

Lambda의 즉시 호출


다음 코드를 확인해보자.

[&] {
    std::cout << "Hello";
}();

해당 코드는 lambda를 정의한 직후 바로 호출한다.

auto lambda = [&] {
    std::cout << "Hello";
};
 
lambda();

두 코드는 같은 방식으로 동작한다. 첫 번째 코드의 마지막 ()가 lambda의 호출 연산자이다.

[&] { ... }에서는 인자 목록 자체가 생략되어 있다. 본문 뒤에 있는 ()는 빈 인자 목록을 선언하는 문법이 아니라, 만들어진 lambda를 호출하는 표현식이다.

Fold expression에서 lambda 즉시 호출하기


가변 인자 템플릿의 parameter pack을 순회하듯 처리할 때 lambda와 fold expression을 함께 사용할 수 있다.

template <typename... Args>
void print(const Args&... args) {
    ([&] {
        std::cout << args << " ";
    }(), ...);
}

핵심이 되는 부분은 다음과 같다.

([&] {
    std::cout << args << " ";
}(), ...);

먼저 [&] { ... }()는 lambda를 정의하고 즉시 호출하는 하나의 표현식이다. 이 표현식 내부에서 parameter pack인 args를 사용하기 때문에 fold expression에 의해서 각 args에 대한 표현식으로 확장된다.

예를 들어 인자가 1, 2, 3이라면 개념적으로 다음과 같이 전개된다.

([&] { std::cout << 1 << " "; }(),
 [&] { std::cout << 2 << " "; }(),
 [&] { std::cout << 3 << " "; }());

,는 comma operator이다. 왼쪽 표현식을 먼저 평가하고 그 결과를 버린 후 오른쪽 표현식을 평가한다. 따라서 각 lambda가 parameter pack의 순서대로 호출된다.

단순히 값을 출력하는 것만 필요하다면 lambda 없이 다음과 같이 작성할 수도 있다.

((std::cout << args << " "), ...);

lambda를 사용하는 이유는 fold expression의 각 단계에서 여러 문장이나 조건문을 하나의 호출 표현식으로 묶을 수 있기 때문이다.

template <typename... Args>
void printPositive(const Args&... args) {
    ([&] {
        if (args > 0) {
            std::cout << args << " ";
        }
    }(), ...);
}

Capture clause


Capture clause는 lambda 외부에 선언된 변수를 lambda 내부에서 어떤 방식으로 사용할지 지정한다.

[capture](parameters) {
    body
}

주로 사용하는 방식은 다음과 같다.

  • []: 외부 변수를 capture하지 않는다.
  • [&]: lambda에서 사용하는 외부 변수를 reference로 capture한다.
  • [=]: lambda에서 사용하는 외부 변수를 value로 capture한다.
  • [a, &b]: a는 value로, b는 reference로 capture한다.
  • [=, &a]: 기본적으로 value로 capture하고 a만 reference로 capture한다.
  • [&, a]: 기본적으로 reference로 capture하고 a만 value로 capture한다.
  • [this]: 현재 객체의 this pointer를 value로 capture한다.
  • [*this]: 현재 객체 자체를 복사하여 capture한다. C++17부터 사용할 수 있다.

예시를 확인해보자.

int a = 10;
int b = 20;
 
auto lambda = [a, &b] {
    // a는 복사된 값을 사용한다.
    // b는 원본을 reference로 사용한다.
    b += a;
};
 
lambda();

[&] 또는 [=]를 사용한다고 해서 현재 scope의 모든 변수가 무조건 closure object에 저장되는 것은 아니다. 일반적으로 lambda 본문에서 실제로 필요한 외부 변수가 암시적으로 capture된다.

this capture 시 주의점

[&, this]는 사용할 수 있지만 [&]만 사용한 것과 같은 효과를 가지므로 중복된 표현이다. 호환성을 위해 허용되는 문법이다.

반면 [this, &]는 capture default인 &가 capture list의 앞에 와야 하므로 올바른 문법이 아니다.

[&, this] { /* ... */ }; // 허용되지만 this 표기가 중복됨
[this, &] { /* ... */ }; // 잘못된 문법

C++20에서 deprecated된 것은 [this, &]가 아니라 [=]를 통해 this를 암시적으로 capture하는 방식이다. this pointer를 capture하려는 의도를 명확하게 나타내려면 [=, this]를 사용하고, 현재 객체 자체의 복사본이 필요하다면 [=, *this]를 사용할 수 있다.

정리


  • Lambda는 이름이 없는 함수 객체를 정의하는 문법이다.
  • 인자가 없다면 ()를 생략할 수 있고, 반환 타입이 명확하다면 반환 타입도 생략할 수 있다.
  • Lambda 본문 뒤의 ()는 만들어진 lambda를 즉시 호출한다.
  • Fold expression 안에서 lambda를 호출하면 parameter pack의 각 요소마다 여러 동작을 수행할 수 있다.
  • Capture clause를 통해 외부 변수를 value 또는 reference로 사용할지 지정할 수 있다.

참고


https://eel.is/c++draft/expr.prim.lambda.capture