Lambda(람다)
1. Lamda Syntax Overview
람다는 클로져(closure)를 만들어내는 표현식(expression)으로 보통 람다 표현식 또는 람다 함수라고 부른다.
클로져는 범위 내 변수를 캡쳐 할 수 있는 이름없는 함수 객체이다.
[] (int x, int y) -> int {return x + y; }
//후행 반환 타입은 해당 람다의 본문의 반환 값을 통해서 추론 가능해 생략 가능하다.
[] (int x, int y) { return x + y; }
[]() { std::cout << "hello world"; }
//함수 인자 목록이 없고 지정자나 예외 명세등이 없이 바로 본문이 오면 함수 인자 목록은 생략가능하다.
[] { std::cout << "hello world";}
[&x]() mutable { x = 5; };
//함수 인자와 본문 사이에 mutable 지정자가 있으므로 아래와 같이 생략 불가
[&x] mutable { x= 5;}
[x, y]() -> int { return x * y ;}
//함수 인자와 본문 사이 후행 반환 타입이 있으므로 아래와 같이 생략 불가
[x, y] -> int { return x * y; }
captures : 외부 변수를 람다의 본문(body)에서 사용할 있게 캡쳐할 변수 목록
tparams : 템플릿 가변 인자 리스트
params : 함수 인자 목록
specifier : mutable, constexpr(c++17), consteval(c+++20)
exception : 예외 명세 설정 함수 호출 연산자의 예외명세를 설정
ret : 반환 값
requires : 제약사항 추가
일반적인 람다 함수의 경우 captures, params, specifier, ret 정도만 사용한다.
2. 람다와 클로져
C++11 이전에 사람을 나이 순으로 정렬하기 위해 std::sort() 함수를 이용할 때 객체를 하나 생성하고 함수 연산자를 재정의해서 이를 넘겼다.
#include <iostream>
#include <vector>
struct Person {
std::string name;
int age;
};
struct by_age {
// 함수 호출 연산자 재정의
bool operator()(const Person &a, const Person &b) const {
return a.age <= b.age;
}
};
int main() {
std::vector<Person> people{
{"a", 20},
{"b", 30},
{"c", 5}
};
//by_age객체를 따로 정의하고 해당 생성자를 predicate에 전달
std::sort(people.begin(), people.end(), by_age());
std::vector<Person>::iterator iter;
for(iter = people.begin() ; iter != people.end(); ++iter) {
std::cout << iter->name << " ";
}
std::cout << "\n";
return 0;
}
이를 동일하게 람다를 통해서 아래와 같이 구현 가능하다.
int main() {
std::vector<Person> people{
{"a", 20},
{"b", 30},
{"c", 5}
};
//predicate 를 람다로 직접 전달
std::sort(people.begin(), people.end(), [](const Person& p0, const Person& p1) {
return p0.age <= p1.age;
});
for(const auto& p: people) {
std::cout << p.name << " ";
}
std::cout << "\n";
return 0;
}
람다는 불필요한 클래스를 정의하지 않고 함수 내에서 정의하고 직접 호출 할 수 있고 객체와 동일하게 함수의 파라미터로 코드를 전달 할 수 있다. 따라서 코드가 간략해 지고 가독성이 좋아진다. 또한 컴파일러는 람다의 최적화를 수행해 람다 사용에 대한 비용이 들지 않는다. (const Person& p0, const Person& p1) {...} 은 아래와 같은 객체를 생성하는 데 이를 클로져라 한다.
(여기서는 임으로 클래스 이름을 anonymous_type 이라고 가정한다.)
struct anonymous_type {
//함수 호출 연산자는 기본적으로 const 함수
auto operator() (const Person& p0, const Person& p1) const {
return p0.age <= p1.age;
}
};
3. 람다 캡쳐
람다는 자신의 범위 내에서 접근 가능한 변수를 람다 내부에서 사용할 수 있도록 [] 내에 변수 이름을 전달할 수 있는 데 이를 캡쳐(capture)라 한다.
#include <iostream>
#include <vector>
int main() {
std::string message = "hello world";
//lambda 는 message를 캡쳐해 본문(body)에서 사용할 수 있다.
//message는 복사를 통해 람다 본문에서 사용하므로 두 message는 다른 객체 임
auto lambda = [message]{ std::cout << message; };
lambda();
return 0;
}
캡쳐는 [ ] 사이에 캡쳐할 사이에 변수를 콤마(,)로 구분하여 캡처할 목록을 지정할 수 있다. 여기서는 message를 캡쳐하는 데 기본적으로 캡쳐는 값 복사를 통해서 이루어진다.
위 람다는 아래와 같은 이름 없는 함수 객체를 생성하는 데 아래에서 보는 것과 같이 캡쳐한 값을 저장할 멤버 변수가 만들어지고 멤버 변수는 생성자의 인자로 전달된다. 기본적으로 캡쳐는 값을 복사해서 이루어진다.
struct anonymous_type {
//캡쳐할 값을 저장할 변수
std::string message;
//캡쳐한 값은 함수 객체의 생성자로 복사를 통해서 전달
anonymous_type(std::string message) : message(message) {}
//함수 호출 연산자는 const 함수로 멤버 변수를 변경할 수 없다.
auto operator () () const {
std::cout << message;
}
};
anonymous_type lambda(message);
lambda();
람다 본문 내 에서 캡쳐된 외부 변수 값을 변경하고 자 할 때는 참조를 통해 캡쳐를 해야하는 데 이때 변수 이름 앞에 &를 사용한다.
int main() {
std::string message = "Hello world";
//message는 참조를 통한 캡쳐로 이제 람다 본문에서 수정 가능
auto lambda = [&message](){ message = "Bye world";};
lambda();
std::cout << message << std::endl; //"Bye world" 출력
return 0;
}
참조를 통한 캡쳐는 참조 멤버 변수를 만들고 생성자를 통해서 전달된다.
struct anonymous_type {
std::string& message;
//생성자는 message를 참조
anonymous_type(std::string& message) : message(message) {}
auto operator () () const {
message = "Bye world";
}
};
람다는 외부 변수를 캡쳐하기 위해 여러 구문을 지원한다.
[ = ] 람다 가시 범위 내 모든 외부 변수를 값으로 갭쳐한다 [ & ] 람다 가시 범위 내 모든 외부 변수를 참조로 캡쳐한다. [ =, &a ] 람다 가시 범위 내 a는 참조로 나머지 변수들은 값으로 캡쳐한다. [ &, a ] 람다 가시 범위 내 a는 값으로 나머지는 참조로 캡쳐한다. |
#include <iostream>
int main() {
int a = 10, b = 20, c = 30;
//1. 람다 가시 범위 내 모든 값을 복사
auto capture_all_by_copy = [=] (int x, int y){
return a*x + b*y + c;
};
std::cout << capture_all_by_copy(10, 20) << std::endl;
//2. 람다 가시 범위 내 모든 값을 참조
auto capture_all_by_referece = [&]( int x, int y) {
a = x;
b = y;
};
capture_all_by_referece(2, 3);
std::cout << "a= " << a << ", b= " << b << std::endl;
//3. a 참조로 나머지는 값으로 복사
auto caputre_by_capy_without_a = [=, &a]() mutable {
a = 0;
b = 10;
c = 30;
};
caputre_by_capy_without_a();
//b 와 c 는 복사된 값으로 변경되지 않음
std::cout << "a= " << a << ", b= " << b << ", c= " << c << std::endl;
return 0;
}
3번째 예에서 복사된 값을 변경하기 위해 mutable 지정자가 설정되어 있음을 알 수 있다. 이는 람다 함수는 기본적으로 함수 호출 연산을
const 함수로 만들어 내기 때문인데 이는 복사로 캡쳐된 변수 값은 멤버 변수가 되기 때문에 이를 const 함수에서 변경할 수 없기 때문이다.
// 람다함수는 암묵적으로 const라 멤버 변수를 변경할 수 없으므로
// 컴파일 에러가 발생한다.
auto caputre_by_capy_without_a = [=, &a]() {
a = 0;
b = 10;
c = 30;
};