-
Lambda(람다)Modern C++/Lambda 2020. 7. 15. 02:52
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; };