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]: 현재 객체의thispointer를 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로 사용할지 지정할 수 있다.