람다 표현식
은 람다식
, 람다 함수
로도 부르지만, C++
에서 정확한 용어는 람다 표현식(lambda expression)
이다. 함수형 언어나 파이썬 같은 인터프리터를 사용하는 언어에서 사용되던 클로저(closure)
가 네이티브 프로그래밍 언어에 도입되면서 람다 함수
, 람다 표현식
이 되었다.
C++
언어에서 람다 표현식
은 함수를 단순하게 만들고 로직을 이해하기 쉽게 만들어 준다. 공통 기능을 함수로 만들면 여러 상황을 고려해야 하므로 예외 처리, 매개변수에 따른 분류등 부수적인 코드가 필요하다.
하지만 람다 표현식
을 즉시 호출 형태로 사용하면 코드의 위치가 곧 호출 위치이므로 로직을 한눈에 파악할 수 있으며, 인라인(inline)
함수로 만들어 성능을 최적화하는 데도 도움이 된다.
람다 표현식
은 다양한 형태로 사용되지만, 기본 구조는 정의부를 포함해 크게 5개로 구성한다. 이를 코드로 나타낸 예를 살펴보자.
다음 코드는 거스름돈을 반환하는 람다 표현식
을 즉시 호출 형태
로 표현한 예이다.
[&changes](int payment, int price) -> int {
int change = payment - price;
changes[0] = change / 1000;
change %= 1000;
return payment - price;
} (payments[i], price[i])
매개변수 목록과 정의부는 일반 함수와 같다. 다른 점은 이름이 없다
는 것과 외부 변수를 캡쳐하는 부분
, 그리고 반환값의 데이터 형식을 화살표 연산자(->)
다음에 명시한다는 점이다. 그리고 람다 함수를 즉시 호출 형태로 사용하려면 정의부 다음에 호출 연산자와 함께 매개변수에 맞춰 인자를 전달한다.
함수 이름이 없다는 특성에 따라 람다 함수를
익명 함수(anonymous function)
라고도 한다.
람다 표현식
은 이를 선언한 함수의 범위에서 사용되지만, 하위 함수로 선언되므로 상위 함수의 지역인 선언부의 변수에는 접근할 수 없다. 외부 변수 캡쳐
는 상위 함수에 선언된 지역 변수를 람다 표현식 내에서 사용할 수 있게 해준다.
외부 변수를 캡쳐하는 방법은 다음 표에서 확인할 수 있다. 표에서 선언부 범위
란, 람다 표현식
이 선언된 함수를 의미한다. 예를 들어 main 함수에서 람다 함수를 선언했다면 main 함수에 선언된 지역 변수를 컙쳐한다. 만약, main 함수에서 생성한 객체의 멤버 함수에서 람다 표현식
을 선언했다면 해당 객체의 멤버 함수에서 선언된 지역 변수를 캡쳐한다.
캡쳐 방법 | 캡처되는 변수 | 람다 표현식 내 사용 |
---|---|---|
[=] | 선언부 범위의 모든 변수 | 읽기 전용으로 사용 |
[&] | 선언부 범위의 모든 변수 | 참조 형식으로 사용되어 읽기와 쓰기 가능 |
[변수1] | 변수1 | 변수1을 읽기 전용으로 사용 |
[변수1, 변수2] | 변수1, 변수2 | 변수1, 변수2를 읽기 전용으로 사용 |
[&변수1, 변수2] | 변수1, 변수2 | 변수1은 참조 형식으로, 변수2는 읽기 전용으로 사용 |
[=, &변수1] | 선언부 범위의 모든 변수 | 변수1은 참조 형식으로, 나머지는 읽기 전용으로 사용 |
[&, 변수1] | 선언부 범위의 모든 변수 | 변수1은 읽기 전용으로, 나머지는 참조 형식으로 사용 |
= (읽기 전용 캡처)
이나 & (참조 캡처)
를 사용하면 선언부 범위의 지역 변수를 모두 캡쳐할 수 있으며, 이와 함께 지역 변수 이름을 나열하면 전체 지역 변수 캡처와 반대되는 방식(읽기 전용 <-> 참조)으로 캡처된다.
람다 표현식
을 함수 객체
로 사용할 때는 호출부를 작성하지 않는다. 함수 객체
로 선언한 람다 표현식
은 호출이 필요할 때 함수 객체
를 호출해서 사용한다.
// 함수 객체로 사용하는 형태
auto calcu_changes = [&changes](int payment, int price) -> int {
int change = payment - price;
changes[0] = change / 1000;
change %= 1000;
return payment - price;
}
함수 객체
는 다른 함수나 객체에 매개변수로 전달할 수 있다. 그런데 변수를 캡쳐했을 때는 범위를 벗어나서 뎅글링 포인터(dangling pointer)
가 될 수 있으므로 람다 표현식
을 인자로 전달할 때는 변수를 캐쳐하지 않는 것이 좋다.
렝글링 포인터(dangling pointer)란?
렝글링 포인터는 이미 해제된 메모리의 주소가 포인터에 저장된 경우를 의미한다. 이미 해제된 메모리이므로 다른 포인터가 사용하거나 쓰레기 값이 담겨 있을 수 있다.
람다 표현식
에서 클래스의 멤버 변수를 사용하고자 할 때 this 포인터
를 캡쳐하는 방법이 있다.
=
이나 &
로 전체 변수를 캡처하면 this 포인터
도 함께 캡쳐된다. 캡쳐한 변수를 사용할 때는 this
를 명시하면 된다.
[&](int payment) -> int {
int change = payment - this->price[i];
...
return payment - this->price[i];
}
지금까지 알아본 람다 표현식
에서는 다양한 방법으로 외부 변수를 캡처해 사용할 수 있다. 그런데 만약 외부 변수를 람다 표현식 내에서만 변경해서 사용하고 외부에는 영향을 주지 않으려면 어떻게 해야 할까? 참조 형식이나 읽기 전용으로는 어려울 것이다.
이때는 변수나 객체를 복사해서 가져올 수 있는 mutable
키워드를 이용한다.
람다 표현식
을 선언할 때mutable
키워드를 이용하면람다 표현식
내에서는 변경할 수 있지만, 외부 변수에는 영향을 미치지 않도록 변수가복사
된다.
즉, 읽기 전용도 아니고 참조 형식도 아니므로 변경 가능하면서 외부에는 영향을 미치지 않는다. mutable
키워드는 매개변수 목록 다음에 추가하는 선택 사항이다. 만약 this
포인터도 복사해야 한다면 *this
로 캡처하면 된다.
또한 람다 표현식
내부에서 예외 사항일 발생했을 때 동적 예외 처리 식별자를 사용할 수 있다. 예외 처리를 나타내는 throw
를 -> 연산자
앞쪽에 추가할 수 있다.
[&changes] (int payment, int price) mutable throw() -> int {
...
} (payments[i], price[i])
[&changes]
(int payment, int price)
mutable
값에 의한 호출(call by value)
처럼 사용할 때 지정한다.throw()
-> int
...
(payments[i], price[i])