lambda
lambda는 람다 표현식 또는 람다 함수, 그리고 이름 없는 함수(anonymous function)라고 불리우며, 그 성질은 "함수 객체(functor)와 동일하다" 할 수 있다.
많은 프로그래밍 언어들이 이름없는 함수 컨셉을 지원한다. 이름없는 함수는 말 그대로 body는 가지나 이름이 없는 함수를 의미한다. lambda 함수는 암시적으로 함수 객체 클래스를 정의하고, 그 함수 객체를 생성한다.
참고로, lambda는 특별한 타입을 가지고 있다고 하나, decltype, sizeof 는 사용할 수 없다고 한다.
1. 함수객체 vs lambda
코딩하다 보면, 함수 포인터와 함수 객체를 사용해야 할 때가 있다. 함수 포인터와 함수 객체 모두 아래와 같은 장점과 단점이 존재한다.
함수 포인터는 작성이 가장 단순하나, 상태를 가지지 못한다. 함수 객체는 상태를 가질 수 있으나, 클래스나 구조체를 정의해야 하는 번거로움이 있다.
lambda 함수는 함수 포인터와 함수 객체의 단점은 배제하고, 장점을 모두 가지고 있다. lambda 함수는 작성하기 용이하며(구조체 정의 작성 따윈 필요없다), 상태 역시 가질 수 있다.
우선 간단한 예를 들어, 함수 객체에 비해 lamba가 얼마나 편리한 것인지 확인해 보도록 하자.
#include <algorithm>
#include <vector>
using namespace std;
// 함수 객체를 위한 구조체 정의
struct EvenCountFunctor : public unary_function<int, void>
{
EvenCountFunctor(int& count) : evenCount(count) {}
void operator () (int number)
{
if (number % 2 == 0) { evenCount++; }
}
private:
int& evenCount;
};
int main()
{
vector<int> v;
for (int i = 0; i < 10; ++i)
{
v.push_back(i);
}
int evenCount = 0;
// 함수 객체
for_each(v.begin(), v.end(), EvenCountFunctor(evenCount));
evenCount = 0;
// lambda 함수
for_each(v.begin(), v.end(), [&evenCount] (int n) {
if (n % 2 == 0) { evenCount++; }
});
}
2. lambda의 기본 사용법
2-1. lambda의 기본 문법
lambda의 기본 문법은 다음과 같이 구성되어 있다.
[] // lambda capture 영역
() // parameter list 영역
{} // body 영역
() // 이는 위 lambda 함수를 명시적으로 호출할 때
int main()
{
// no capture, no parameter
[] { std::cout << “Welcome to lambda...” << std::endl; } ();
}
2-2. lambda의 parameter
#include <iostream>
int main()
{
[] (int n) { std::cout << "Number : " << n << std::endl; } ();
}
2-3. lambda를 tr1::function에 대입
lambda는 tr1::function으로 대입이 가능하다.
auto 키워드를 쓰면 더욱 편리하게 대입이 가능하다.
#include <iostream>
int _tmain(int argc, _TCHAR* argv[])
{
auto lambaFunc = [] (int n)
{
std::cout << "Number : " << n << std::endl;
};
lambaFunc(100);
return 0;
}
2-4 lambda를 함수의 파라미터로 사용
lambda를 tr1::function으로 대입이 가능하다는 것은 템플릿 함수의 인자로써 lambda를 사용할수도 있고, 어떤 객체의 상황별 콜백 함수를 불러줄 때 사용을 해도 괜찮다. (단, 콜백함수 개수가 너무 많지 않아야 한다.)
template<typename TFunc>
void TemplateFunc(TFunc func)
{
func();
};
int _tmain(int argc, _TCHAR* argv[])
{
auto func = [] { std::cout << "Hello World" << std:: endl; };
TemplateFunc(func);
return 0;
}
2-5. lambda를 반환하는 함수
lambda를 함수의 인자로 받는 것과 마찬가지로, 함수가 lambda를 반환하는 것도 가능하다.
#include <iostream>
#include <functional> // for tr1::function
using namespace std;
using namespace std::tr1;
// void 반환, 인자 없음의 함수타입을 반환한다
function<void ()> FunctionReturn()
{
return [] { cout << "대한민국~~~짝짝짝 짝짝~~~" << endl; };
}
int _tmain(int argc, _TCHAR* argv[])
{
auto func = FunctionReturn();
func();
return 0;
}
2-6. lambda 함수를 STL container에 저장
#include <algorithm>
#include <functional>
#include <vector>
using namespace std;
using namespace std::tr1;
int main()
{
vector<function<const char* ()> > v;
v.push_back( [] { return "대한민국 "; } );
v.push_back( [] { return "짝짝짝~~~짝짝~~~"; } );
printf("%s %s\n", v[0](), v[1]());
}
2-7. lambda의 반환값 타입 명시
lambda는 값을 반환할 수 있으며, 반환값의 타입을 명시적으로 지정할 수 있다. c++0x 부터 도입된 trailing return type(후행 반환 형식)을 사용하는 것이다.
또한, 함수 포인터와는 다르게 암묵적으로 타입을 추론할 수도 있다.
int _tmain(int argc, _TCHAR* argv[])
{
// 암시적 타입 반환
auto func1 = [] { return 3.14; };
// 후행 반환 형식을 이용한 명시적 반환값 타입 지정 ( [ -> (return type) ] )
// 파라미터가 없더라도 반드시 파라미터 블록을 지정해 주어야 한다
auto func2 = [] () -> float { return 3.14f; };
// func2의 반환값은 double 타입으로 추론되었다. 아래 경고 발생
// warning C4244: '초기화 중' :
// 'double'에서 'float'(으)로 변환하면서 데이터가 손실될 수 있습니다.
float f1 = func1();
// 명시적으로 float 반환했기에 OK
float f2 = func2();
return 0;
}
3. lambda의 capture
lambda 외부에 정의되어 있는 변수를 lambda 내부에서 사용하고 싶을 때 그 변수를 capture 한다.
Capture는 참조나 복사 방식 모두 지원하며, 참조는 '&variable' 로, 복사는 'variable'로 기술한다.
Capture는 위 문법에서 살펴 보았듯이 [] capture 영역에 기술한다.
3-1. 변수를 참조로 캡쳐
이 예제는 위 lambda 소개에서 써먹었던 예제를 재활용하겠다. 아래 예제에서는 evenCount 변수를 lambda 함수가 참조로 캡쳐하였다
#include <algorithm>
#include <vector>
using namespace std;
int main()
{
vector<int> v;
for (int i = 0; i < 10; ++i)
{
v.push_back(i);
}
int evenCount = 0;
// lambda 함수
for_each(v.begin(), v.end(), [&evenCount] (int n)
{
if (n % 2 == 0) { evenCount++; }
});
}
int evenCount = 0;
int* pEvenCount = &evenCount;
for_each(v.begin(), v.end(), [pEvenCount] (int n)
{
if (n % 2 == 0) { (*pEvenCount)++; }
});
3-2. 변수를 복사로 캡쳐
int evenCount = 0;
// lambda 함수
for_each(v.begin(), v.end(), [evenCount] (int n)
{
if (n % 2 == 0) { evenCount++; }
});
위의 코드를 컴파일 하면 에러 메시지를 뿜으며 컴파일이 되지 않는다.
error C3491: 'evenCount': 변경 불가능한 람다에서 값 방식 캡처를 수정할 수 없습니다.
꼭, 복사로 캡쳐한 변수를 lambda 내부에서 변경을 해야 한다면, mutable 키워드를 사용할 수 있다.
int evenCount = 0;
// lambda 함수
for_each(v.begin(), v.end(), [evenCount] (int n) mutable
{
if (n % 2 == 0) { evenCount++; }
});
3-3. 복수의 변수 캡쳐
복수의 변수 역시 캡쳐가 가능하다.
[] capture 영역에 함수 파라미터 선언하듯이 쭈욱 선언하면 문제될 것이 없다. 또한, 어떤 녀석은 참조로, 어떤 녀석은 복사로 캡쳐하도록 하는 것도 아무 문제가 없다.
int a;
short b;
float c;
char d;
// a, d 참조로... b, c 복사로...
auto func = [&a, b, c, &d] { blah~blah~blah~};
또한, lambda 바깥 함수의 범위보다 더 넓은 범위의 영역(전역 포함)에 있는 모든 변수들을
- 참조로 캡쳐하고 싶을 땐 [&]
- 복사로 캡쳐하고 싶을 땐 [=] 로 사용한다.
- [&, x] reference 복사 x제외
- [=, &y] 모두 복사 y는 ref
[&]와 [=]는 생각보다 자주 사용되니, 꼭 기억해 두길 바란다.
그리고 이건 뭐 한 눈에 딱 봐도 에러인데, 같은 변수를 두 번 캡쳐하려 하면 아래와 같은 컴파일 에러가 발생한다.
error C3483: 'outer_variable xxx'은(는) 이미 람다 캡처 목록의 일부입니다.
int _tmain(int argc, _TCHAR* argv[])
{
int a, b, c;
// default 캡쳐모드는 참조, a와 b는 복사 캡쳐. OK
[&, a, b] {};
// default 캡쳐모드는 복사, c는 참조 캡쳐. OK
[=, &c] {};
// default 캡쳐모드가 참조인데, b를 또다시 참조로 캡쳐.
// error C3488: 기본 캡처 모드가 참조 방식인 경우 '&b'을(를) 사용할 수 없습니다.
[&, &b] {};
// default 캡쳐모드가 복사인데, a와 c를 또다시 복사로 캡쳐
// error C3489: 기본 캡처 모드가 값 방식인 경우 '&a'이(가) 필요합니다.
[=, a, c] {};
return 0;
}
3-4. 전역변수 캡쳐시 주의사항
아래와 같이 전역 변수를 사용하려 하면, 다음 에러가 발생한다.
'g_evenCount': 람다 캡처 변수는 바깥쪽 함수 범위에 속해야 합니다
#include <algorithm>
#include <vector>
using namespace std;
int g_evenCount = 0;
int main()
{
vector<int> v;
for (int i = 0; i < 10; ++i)
{
v.push_back(i);
}
// lambda 함수
for_each(v.begin(), v.end(), [&g_evenCount] (int n)
{
if (n % 2 == 0) { g_evenCount++; }
});
}
g_evenCount는 lambda의 바깥쪽 함수(main 함수) 범위를 벗어난 전역에 위치하고 있다.
이런 경우엔 lambda 바깥 함수의 범위보다 더 넓은 범위의 영역(전역 포함)에 있는 모든 변수들을 참조할 수 있는 [&]를 써야 한다.
즉, 코드를 아래와 같이 변경해야 캡쳐가 가능해 진다.
// lambda 함수
for_each(v.begin(), v.end(), [&] (int n)
{
if (n % 2 == 0) { g_evenCount++; }
});
3-5. lambda in class
클래스의 멤버 함수 내에 lambda 함수를 정의하고, 이 lambda 함수에서 해당 클래스의 멤버를 호출하는 것이 가능하다.
클래스 멤버 함수 내 lambda 함수는 해당 클래스에서 friend 함수로 인식하므로, lambda 함수에서 클래스의 private 멤버에도 접근이 가능하다.
클래스의 멤버를 호출할 때는 반드시 ‘this’ 를 캡쳐해야 한다.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class PrimeNumber
{
public:
PrimeNumber()
{
m_primeList.push_back(1);
m_primeList.push_back(2);
m_primeList.push_back(5);
m_primeList.push_back(7);
// ...
}
void PrintPrimeNumbers() const
{
// this를 캡쳐한 것에 주목!!!
for_each(m_primeList.begin(), m_primeList.end(), [this] (int primeNumber)
{
_Print(primeNumber);
});
}
private:
typedef vector<int> PrimeNumberList;
PrimeNumberList m_primeList;
void _Print(int primeNumber) const
{
cout << "The prime number : " << primeNumber << endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
PrimeNumber pn;
pn.PrintPrimeNumbers();
return 0;
}
3-6. Recursive lambda
lamba 함수 역시 함수의 특성을 모두 가지고 있으므로, 재귀(recursive)가 가능하다.
#include <functional>
using namespace std;
using namespace std::tr1;
int _tmain(int argc, _TCHAR* argv[])
{
// function을 사용하였음에 주의하라
function<int (int)> Factorial = [&Factorial] (int num) -> int
{
return num <= 1 ? 1 : num * Factorial(num - 1);
};
// 5 * 4 * 3 * 2 * 1 = 120
int fact5 = Factorial(5);
return 0;
}
흔하디 흔한 팩토리얼 코드이지만, 위 예제에서 중요한 포인트가 하나 숨어 있다.
보통 lambda 함수를 대입시킬 변수의 타입을 tr1::function이 아닌 auto로 잡는다. 헌데, lambda 함수를 recursive 함수로 작성할 땐 반드시 auto가 아닌 tr1::function 타입으로 잡아야 한다.
그렇지 않으면, 아래 예제와 같은 컴파일 에러가 발생한다.
// 1. error C3536: 'Factorial': 초기화되기 전에 사용할 수 없습니다.
// 2. error C3533: 'auto &': 매개 변수에 'auto'가 포함된 형식을 포함할 수 없습니다.
// 3. error C3531: 'Factorial': 형식에 'auto'가 포함된 기호에는 이니셜라이저가 있어야 합니다.
// 4. error C2064: 항은 1개의 인수를 받아들이는 함수로 계산되지 않습니다.
auto Factorial = [&Factorial] (int num) -> int
{
return num <= 1 ? 1 : num * Factorial(num - 1);
};
auto는 타입 추론을 하는 녀석이다. 위 함수에서 auto가 허용되지 않는 것은 아직 타입이 제대로 추론도 되기 전에 재귀 함수를 사용하려 하니 문제가 되는 것이다.
다시 얘기하지만, lambda recursive 함수의 대입 타입으로 auto를 사용하면 안 된다. 제대로 된 std::tr1::function 타입을 사용할 것
3-7. lambda closure
간혹 lambda가 clousure라고 혼동하는 사람들이 꽤 있다.
lambda는 closure가 아니다. 단지 lambda를 이용하여 closure의 특성을 구현해 낼 수 있는 것이다.
아래 예제를 한 번 살펴 보자. 아래 예제의 두 번째 f 호출의 결과는 38인가? 70인가?
#include <iostream>
#include <functional>
using namespace std;
using namespace std::tr1;
int _tmain(int argc, _TCHAR* argv[])
{
int a = 7, b = 3;
// 외부 변수 a와 b를 복사 캡쳐하고, int x를 파라미터로 받는 lambda 함수
// a * x + b 의 결과를 출력한다.
auto closureFunc = [a, b] (int x)
{
cout << a * x + b << endl;
};
closureFunc(5); // 7 * 5 + 3 = 38 를 출력
// 여기에서 a와 b를 각각 7 -> 10, 3 -> 20으로 바꾸었다
a = 10, b = 20;
// 그렇다면 지금의 closureFunc(5)는
// 7 * 5 + 3 = 38 일까? 아니면 10 * 5 + 20 = 70일까?
closureFunc(5);
return 0;
}
두 번째 closureFunc 함수 호출의 결과는 38이 출력된다.
function
다르게 표현하자면, closureFunc 함수는 a 와 b 의 값에 "Closed over(Closure)" 되었다고 할 수 있다. 이와 갈은 변수의 바인딩 방식을 static(lexical) binding 이라 부른다.
Closure 특성이 유용한 이유는 위 예제에서도 볼 수 있듯이 late evaluation 이 가능하다는 것이다.
일단 특정 환경에서 함수를 정의한 후 나중에 함수를 호출하여 실행시킬 수 있는 것이다. 이 때 closure 함수는 바뀐 주변 환경과 관계없이 자신이 정의될 때의 환경에 따라 행동한다.
즉, closureFunc 함수는 언제 어디서 호출되더라도 a = 7, b = 3의 값을 보존하고 있는 것이다.
이러한 closure 의 특징은 하나의 환경 하에서 여러 개의 코드 블록을 정의하여, 이후 해당 함수들이 필요할 때 실행시킬 수 있는 이른바 multiple function 의 사용을 가능하게 한다.
8. 정리
- lambda는 람다 표현식 또는 람다 함수, 그리고 이름 없는 함수라고 불리며, 함수 오브젝트 중 하나이다.
- lambda는 특별한 타입을 가지고 있다고 하지만 decltype 이나 sizeof 는 사용 불가
- lambda를 정의한 곳의 외부 변수를 lambda 내부에서 사용하고 싶을 때는 참조 또는 복사로 캡쳐한다.
- 클래스에서도 lambda를 사용할 수 있으며, 클래스는 lambda를 friend 함수로 인식한다.
- lamba는 closure가 아니다. 단지 lambda를 이용하여 closure의 특성을 구현할 수 있는 것이다.