이번 포스팅에서는 람다 함수를 어떻게 만들어야 하는지 알아보겠습니다.
아래는 세상에서 가장 간단한 람다함수입니다.
[]{};
희한하긴 한데, 좀 싱겁네요. 하나 하나 뜯어보도록 하겠습니다.
1. 람다 함수 원형
[]{};
위의 코드는 아무 일도 하지 않는 람다함수입니다. 하지만 문법상으로는 아무 오류가 없습니다. 일반함수로 치면 void foo(){} 와 동일합니다.
[] : Lambda Introducer 라고 부릅니다. 무조건 이걸로 시작해야 C++11 컴파일러가 알아먹습니다.
{} : Lambda Body 라고 부릅니다. 일반 함수처럼 이 내부에 람다함수가 할 일을 적어넣어야 합니다.
2. 람다 함수 바디
이번엔 람다 바디를 한번 채워보겠습니다.
[]{std::cout<<"Hello World"<<std::endl;};
람다의 헬로월드 격인 예제입니다. 일반 함수로 치면, void foo() { std::cout<<"Hello World"<<std::endl;} 와 동일합니다.
위의 람다 함수는 아래처럼 바꿔쓸 수 있습니다.
[](){std::cout<<"Hello World"<<std::endl;};
아래처럼 써도 됩니다.
[](void){std::cout<<"Hello World"<<std::endl;};
뭐가 달라졌는지 눈치 채셨나요? Lambda introducer 뒤에 () 를 추가했습니다. () 는 람다함수로 파라미터를 넘겨 받기 위해 써줍니다. "Parameter Specificition" 이라고 부르고요. 아무런 파라미터를 받지않는 경우에는 생략하거나 void 로 채워주면 됩니다.
3. 람다함수의 호출
위의 코드들은 람다 함수를 선언/정의(declaration/definition)만 해준 것이고, 실제로 코드 내에서 이 람다함수를 호출하기 위해서는 람다 함수의 바디 맨 끝 부분에 "function operator"인 ()를 붙여줘야 합니다.
아래처럼요.
1 #include <iostream>
2 int main()
3 {
4 []{std::cout<<"Hello World"<<std::endl;}();
5 return 0;
6 }
4. 람다함수 파라미터
이번에는 람다함수에 파라미터를 넘겨봅시다. 예를 하나 들면, 아래와 같습니다.
[](int i){std::cout<<"Passed argument "<<i<<std::endl;};
간단하죠? 일반 함수를 사용하는 것과 같습니다. 단지 일반함수와 다른 점 하나는 default argument parameter를 가질 수 없다는 것입니다.
실제 코드에서 사용하려면, function operator인 ()를 이용해 파라미터를 넘겨주면 됩니다.
아래 처럼요.
1 #include <iostream>
2 #include <string>
3 int main()
4 {
5 [](std::string name){std::cout<<"Hello "<<name<<std::endl;}("World");
6 //print "Hello World"
7 return 0;
8 }
5. 람다함수 리턴값
이번에는 람다함수의 리턴값을 알아봅시다. 예제를 보죠.
bool isTrue = [](){return false;}();
isTrue라는 변수에 false 라는 값을 셋팅했습니다. 사실 이 코드는 아래 코드와 동일합니다.
bool isTrue = []()->bool{return false;}();
->bool 이 추가된 것 말고는 바뀐 점이 없습니다. 바뀐 코드에서는 명시적으로 리턴타입을 지정해준 것입니다.
이런 식으로 람다함수에 명시적인 리턴 타입을 지정할 수 있습니다만, 만약 리턴타입이 생략되면 컴파일러는 람다함수 바디 내부에서 묵시적으로 리턴값을 추정하여 자동으로 셋팅해줍니다. 여기서 중요한 점은 컴파일러가 추정할 수 있어야 합니다! 컴파일러가 추정할 수 있으려면, 람다 함수 바디 내부에서 return statement 를 찾을 수 있어야 하고, 찾을 수 있으려면 return 구문이 바로 등장해야 합니다. 만약 찾을 수 없다면 디폴트 리턴타입인 void 로 셋팅합니다.
아래 코드는 에러입니다.
1 #include <iostream>
2 int main()
3 {
4 bool isTrue = [](int n){
5 if(n > 0)
6 return true;
7 else
8 return false;
9 }(30);
10 return 0;
11 }
12 //error C2440: 'initializing' : cannot convert from 'void' to 'bool'
13 //error C3499: a lambda that has been specified to have a void return type cannot return a value
컴파일러는 "return true;" 나 "return false;" 구문이 if 절 안에 내포되있기 때문에, 묵시적인 리턴 타입 추출에 실패합니다. 바로 찾지를 못하는 거죠.
또한 람다 함수 정의시 리턴 타입이 생략되있으므로, 컴파일러는 리턴타입을 일단 디폴트 타입인 void 로 추정합니다. 그러나 람다 함수 바디 안의 if 절 내부에서 return 을 하는 구문을 만났기 때문에 위와 같은 에러를 내는 겁니다. 따라서 이런 경우엔 명시적으로 리턴타입을 지정해줘야만 합니다.
위코드는 간단히
[](int n)l{
를
[](int n)->bool{
로 고치면 에러가 나지 않습니다.
다른 방법으로는 ternary operator를 사용해서 return 구문을 한 라인으로 바꿔주면, 컴파일러가 묵시적으로 리턴타입을 추론하게 할 수 있습니다.
1 int main()
2 {
3 bool isTrue = [](int n){ return (n>0)? true:false;}(30);
4 return 0;
5 }
6. 람다함수의 Capture Clause
다음으로 Lambda introducer 인 [] 에 대해 알아봅시다. [] 는 다른말로 Capture clause 라고 말합니다.
capture의 사전적 의미는 "Take into one's possession or control by force" 즉, 남의 것을 자기 것으로 만드는 겁니다. 그럼 뭘 캡쳐한다는 걸까요? 람다함수 밖에 선언된 (람다함수를 감싸고 있는 범위 내의) 변수를 캡쳐한다는 의미입니다.
일례로 앞의 Lambda1 의 포스트에서 쓴 예제코드를 다시 보겠습니다.
1 [&](int i)
2 {
3 if(i%2 == 0)
4 evenSum += i;
5 else
6 oddSum += i;
7 }
첫번째 라인에서 [&] 라고 썼는데, 이렇게 쓰면 람다 외부에서 선언된 모든 변수(evenSum 과 oddSum을 포함해서)를 람다함수 내부에서 레퍼런스 타입으로 갖다 쓰겠다는 겁니다. 그렇기 때문에 이 두 변수를 람다 함수 내부에서 따로 정의하지 않았지만 쓸 수 있었던 겁니다. 그리고 또한 레퍼런스로 캡쳐하기 때문에 람다 내부에서 연산한 내용이 그대로 원래 변수의 값에도 반영이 됩니다.
값으로 캡쳐할 수도 있습니다. 외부 변수를 간단히 넘겨받아, 연산만 수행할 때는 값으로 캡쳐해도 됩니다. 그때는 & 를 붙이지 않고 써줍니다. 가령 evenSum, oddSum 을 값으로 넘겨받겠다면, [evenSum, oddSum] 이렇게 써주면 되고, 이때 람다 함수 내부에서 변경된 evenSum 과 oddSum 은 외부에 반영되지 않습니다.
외부의 선언된 변수 x, y를 레퍼런스로 캡쳐하겠다면 [&x, &y] 이렇게 써주면 되고, 이때는 내부의 변경이 그대로 외부로 반영됩니다.
그냥 [] 라고 쓰면 아무 것도 캡쳐하지 않겠다는 뜻이지만, 람다함수는 무조건 [] 로 시작해야 컴파일러가 이걸 람다함수라고 알 수 있기 때문에, 캡쳐할 것이 아무것도 없다 해도 무조건 써줘야 됩니다.
정리하면 이렇습니다.
[] : 아무것도 캡쳐 하지 않음.
[&x] : x 만 레퍼런스로 캡쳐하고, 다른 변수는 캡쳐하지 않음.
[x] : x 만 값으로 캡쳐하고, 다른 변수는 캡쳐하지 않음.
[&] : 모든 외부 변수를 레퍼런스로 캡쳐.
[=] : 모든 외부 변수를 값으로 캡쳐.
[x, y] : x와 y 를 값으로 캡쳐.
[&x, y] : x는 레퍼런스로 y는 값으로 캡쳐.
[x, &y] : x는 값으로 y는 레퍼런스로 캡쳐.
[&x, &y] : x 와 y 를 레퍼런스로 캡쳐.
[&, x] : x를 제외한 모든 변수를 레퍼런스로 캡쳐. (x는 값으로 캡쳐)
[=, &x] : x를 제외한 모든 변수를 값으로 캡쳐. (x는 레퍼런스로 캡쳐)
다음은 에러입니다.
[=, &] : 모든 값을 값 또는 레퍼런스로 캡쳐하려면 둘중의 하나를 선택해야 합니다.
[&x, =] : = 가 먼저 나와야 합니다.
[x, &] : & 가 먼저 나와야 합니다.
* 스태틱 변수는 캡쳐할 수 없습니다. automatic storage duration 을 갖는 일반 변수만 캡쳐할 수 있습니다.
7. 람다함수의 mutable 키워드
값으로 캡쳐된 외부 변수는 const의 특징을 갖기 때문에 람다 바디 내에서 수정할 수 없습니다. 그러나, mutable 이라는 키워드를 통해 값으로 캡쳐된 외부 변수라도 람다 바디내에서 수정이 가능합니다.
하지만, 이렇게 바디내에서 수정이 가능하더라도 값으로 캡쳐되었기때문에 여전히 외부 변수에 반영되지는 않습니다.
int x = 0;
[=]()mutable
{
x++;
}();
mutable 키워드를 사용하려면 파라미터 스펙("()" 기호)을 반드시 명시적으로 지정해줘야 합니다.
8. 람다 함수 활용
람다 함수는 auto 키워드를 이용해 변수에 저장할 수 있고, 또한 람다 함수 내부에서 다른 람다 함수를 호출 할 수 있습니다.
1 #include <iostream>
2 #include <string>
3 #include <array> //array
4 #include <algorithm> //sort
5 #include <functional> //less
6
7 int main()
8 {
9
10 std::array<int, 10> arr = {3,2,1,4,9,10,7,8,4,6};
11
12 //Assign lambda function to variable using "auto" keyword.
13 auto ShowOriginalArray = [=]()
14 {
15 std::for_each(arr.cbegin(), arr.cend(), [](int n)
16 {
17 std::cout<<n<<",";
18 });
19 std::cout<<std::endl;
20 };
21 //call lambda function
22 ShowOriginalArray();
23
24 //sort array
25 std::sort(arr.begin(), arr.end(), std::less<int>());
26
27
28 //show array after sort
29 auto ShowSortedArray = [=]()
30 {
31 std::for_each(arr.cbegin(), arr.cend(), [](int n)
32 {
33 std::cout<<n<<",";
34 });
35 std::cout<<std::endl;
36 };
37 ShowSortedArray();
38
39 //show original array
40 ShowOriginalArray();
41
42 return 0;
43 }
44
라인 13을 보면, 정의한 람다함수를 ShowOriginalArray 변수에 저장한 것을 알 수 있습니다. 그리고 라인 22에서 () 연산자를 이용해 이를 호출하고 있습니다.
라인 13 부터 라인 20 을 보면, 람다함수 내부에서 또 다른 람다 함수를 호출하는 것을 알 수 있습니다.
또한 람다함수에서 값으로 캡쳐하여 연산한 결과는 외부 변수의 변화와 관계없이 그 값이 유지됩니다.
아래 실행결과를 보면 배열인 arr 이 sort 되었지만, ShowOriginalArray() 호출은 여전히 sort 되기 전의 값을 보여주는 것을 알 수 있습니다.
왜 람다함수가 functor 를 계승했다고 하는지를 여실히 보여주는 예입니다. Functor 와 마찬가지로 Lambda도 State 를 가질 수 있다는 겁니다.