반응형

http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=8308


[C++0x] Lambda Expressions and Closures

 

작성일 : 2008.11.23       

작성자 : 아이오 교육센터

www.ioacademy.co.kr

 

글 순서

1. 기본개념

2. 람다 표현식(Lambda expressions)과 and 클로져(closure)

3. 람다 표현식과 리턴 타입

4. Capturing Local Variable

5. Nullary Lambda

6. Mutable lambda

 
1. 기본 개념

 STL의 알고리즘 중에는 함수 객체를 인자로 받는 것이 있습니다. for_each() 알고리즘이 대표적인 경우 입니다. 아래의 코드는 for_each()를 사용해서 주어진 구간의 모든 요소를 화면에 출력하는 코드 입니다.

01: #include <iostream>
02: #include <algorithm>
03: using namespace std;
04: 
05: struct ShowFunctor
06: {
07:         void operator()(int n) const
08:         {
09:                 cout << n << endl;
10:         }
11: };
12: int main()
13: {
14:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
15:         for_each( x, x+10, ShowFunctor() );
16: }

 

 

 그런데 위 코드에 있는 ShowFunctor 같은 함수를 자주 만들어야 한다면 좀 번거로운 일입니다. C++0x는 ShowFunctor 와 같은 함수객체를 보다 쉽게 만들 수 있는 “람다 표현식(Lambda express)”라는 개념을 제공합니다. 아래 코드는 위 코드와 동일한 일을 수행합니다. 단, 완전한 함수객체를 만들어 사용하지 않고 람다 표현식을 사용하고 있습니다.

 
1: int main()
2: {
3:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
4:         for_each( x, x+10, [](int n){ cout << n << endl;} );
5: }

 위 예제를 포함한 이 글에 나오는 모든 예제들은 VS2010CTP 를 사용해서 test 되었습니다.

 

 위 코드 중 다음의 표현이 람다 표현식 입니다.

      [](int n){ cout << n << endl;}

 람다 표현식을 보고 컴파일러는 함수객체 클래스를 정의하고 객체를 생성하게 됩니다. 즉, 위 코드는 아래의 코드와 유사한 의미를 가지고 있습니다.

 
01: class closure_object
02: {
03: public:
04:         void operator()(int n) const
05:         {
06:                 cout << n << endl;
07:         }
08: };
09: int main()
10: {
11:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
12:         for_each( x, x+10, closure_object() );
13: }
 

람다 표현식에 의해 컴파일러가 생성한 함수객체는 클로져 객체(Closure Object) 라고 부릅니다.클로져 객체는 함수호출연산자, 생성자, data멤버를 가진 함수객체와 유사하게 동작합니다.

 

람다 표현식을 간단히 살펴 보도록 하겠습니다..

 
 - [] 는 “lambda introducer” 라고 부르는데, 람다 표현식이 시작됨을 나타냅니다.
 - (int n) 은 “람다 파라미터 선언(lambda parameter declaration)” 이라고 부르는데, 클로져 객체의 함수호출 연산자(즉, ()연산자)가 가져야 하는 파라미터 목록을 컴파일러에게 알려주는 것입니다.
 - 마지막으로 “{ cout << n << endl;}” 는 클로져 객체의 함수 호출연산자의 구현부를 정의 하는 것입니다.
 - 디폴트로 람다 표현식의 리턴 타입은 void를 리턴 합니다.
 - 여러 개의 람다 표현식은 각각 자신만의 타입을 가지게 됩니다.
 

물론, 람다 표현식을 사용하지 않고 완전한 함수객체를 만들어 사용하면 되지만 람다 표현식을 사용하는 것이 좀더 편리 합니다.

 
2. Lambda Expression and Closure
 

[] 를 사용해서 익명의 함수객체를 명시하는 표현 식을 “람다 표현식(lambda expression)” 또는 “람다 함수(lambda function)” 라고 합니다. 또한, 람다표현식의 결과로 컴파일러에 의해 자동으로 생성된 익명의 함수객체(unnamed function object) 를 “클로져(Closure)” 라고 합니다.

 

람다 표현식의 구현은 여러 줄로 구성될 수 도 있습니다.

 
01: #include <iostream>
02: #include <algorithm>
03: using namespace std;
04: 
05: int main()
06: {
07:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
08: 
09:         for_each( x, x + 10, [](int n ) {
10:                 if ( n % 2 == 0 )
11:                         cout << "even ";
12:                 else
13:                         cout << "odd ";
14:         });
15: 
16:         cout << endl;
17: }
 

3. Lambda Expression and return type

 

람다 표현식은 리턴값은 가질수도 있습니다. 아래의 예제를 보겠습니다

 
01: #include <iostream>
02: #include <algorithm>
03: #include <vector>
04: using namespace std;
05: 
06: int main()
07: {
08:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
09:         vector<double> v;
10: 
11:         transform( x, x+10, back_inserter(v), [](int n) -> double {
12:                 if ( n % 2 == 0 ) return n;
13:                 return n / 2.0;
14:         });
15: 
16:         for_each( v.begin(), v.end(), [](double n) { cout << n << " ";});
17:         cout << endl;
18: }
 

“->double” 은 “lambda-return-type-clause” 라고 부르는데, 람다 표현식에서 리턴 타입을 지정하는 구문입니다.

 

아래와 같은 경우, 리턴 타입을 지정하는 것은 생략할 수 있습니다.

 

1. 람다 표현식이 리턴하는 값이 없을 경우 즉, “->void”는 생략이 가능합니다.

2. 람다 표현식이 한 개의 return 문장만을 가지고 있을 경우. 컴파일러는 해당 return 표현식 으로 부터 리턴 타입을 추론할 수가 있습니다.

 
01: #include <iostream>
02: #include <algorithm>
03: #include <vector>
04: using namespace std;
05: 
06: int main()
07: {
08:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
09:         vector<double> v1(10);
10:         vector<double> v2(10);
11: 
12:         // return 문장이 한 개 입니다. "?>double" 는 생략 가능 합니다.
13:         transform( x, x+10, v1.begin(), [](int n) { return n / 2.0;});
14: 
15:         // return 문이 없습 니다. “?>void” 는 생략 가능 합니다.
16:         for_each( v1.begin(), v1.end(), [](double d) { cout << d << " ";});
17:         cout << endl;
18: 
19:         // 2개 이상의 return 문이 있습니다. 반드시 "?>double" 을 표시 해야 합니다.
20:         transform( x, x+10, v1.begin(), [](int n)->double {
21:                 if ( n % 2 == 0 ) return n;
22:                 return n / 2.0;
23:         });
24: 
25:         for_each( v1.begin(), v1.end(), [](double d) { cout << d << " ";});
26:         cout << endl;
27: 
28:         // 역시 1개의 return 문 입니다.
29:         transform( x, x+10, v1.begin(), v2.begin(), [](int a, double d){ return a + d;});
30:         for_each( v2.begin(), v2.end(), [](double d) { cout << d << " ";});
31:         cout << endl;
32: }
 
 

4. Capturing Local Variable

 

  C++에서 함수 객체를 사용하는 이유 중의 하나는 상태를 가질 수 있다는 점입니다. 지금까지의 람다 표현식은 상태를 가지지 않았습니다. 하지만 지역변수를 캡쳐(capture) 하므로서 람다 표현식도 상태를 가질수가 있습니다. 아래 예제를 보겠습니다.

01: int main()
02: {
03:         int x[10] = { 1,3,5,7,9,11,13,15,17,19};
04: 
05:         int low = 12;
06:         int high = 14;
07: 
08:         int* p = find_if( x, x+10, [low, high](int n) { return low < n && n < high;});
09: 
10:         cout << *p << endl;
11: }
 

 Lambda introducer 인 “[]” 안에는 지역변수를 넣으 므로서 해당 지역변수를 람다 표현식안에서 사용할수있습니다. 이러한 것을 “capture list” 라고 합니다. “[low, high]”라고 표시하므로서 low, high 지역변수를 람다표현식 안에서 사용할 수 있습니다. “capture list”를 사용하지 않을 경우 람다 표현식 안에서 지역변수에 접근할 수는 없습니다.

 

결국 위 표현식은 아래코드와 같은 의미를 가지게 됩니다.

 
01: class closure_object
02: {
03: private:
04:         int min;
05:         int max;
06: public:
07:         closure_object( int l, int h ) : min(l), max(h) {}
08:         bool operator()(int n) const { return min < n && n < max; }
09: };
10: 
11: int main()
12: {
13:         int x[10] = { 1,3,5,7,9,11,13,15,17,19};
14:         int low = 12;
15:         int high = 14;
16: 
17:         int* p = find_if( x, x+10, closure_object(low, high) );
18:         cout << *p << endl;
19: }
 

디폴트 캡쳐인 “[=]”를 사용하면 모든 지역 변수를 람다 표현식에서 사용할 수 있습니다.

 
1: int main()
2: {
3:         int x[10] = { 1,3,5,7,9,11,13,15,17,19};
4:         int low = 12;
5:         int high = 14;
6: 
7:         int* p = find_if( x, x+10, [=](int n) { return low < n && n < high;});
8:         cout << *p << endl;
9: }
 

“[low, high”] 또는 “[=]” 는 모두 지역변수를 값으로(by value)로 캡쳐 합니다. 아래 와 같이 사용 하므로서 지역변수를 참조(by reference)로 캡쳐 할 수 있습니다. 즉, 람다 표현식 안에서 지역변수의 값을 변경 할 수 있습니다.

 
1: int main()
2: {
3:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
4:         int sum = 0;
5: 
6:         // x~x+10 에서짝수의합을구하는람다표현식입니다.
7:         for_each( x, x+10, [&sum] (int n) { if ( n % 2 == 0 ) sum += n;});
8: 
9:         cout << sum << endl;
10: }
 

 “[&sum]” 은 지역변수 sum 을 람다 표현식에서 참조로 사용하겠다는 의미 입니다. 즉, 아래의 코드와 동일한 의미를 가지게 됩니다.

 
01: class closure_object
02: {
03: private:
04:         int& ref;
05: public:
06:         closure_object ( int& r ) : ref(r) {}
07:         void operator()(int n) const { if ( n % 2 == 0 ) ref += n; }
08: };
09: 
10: int main()
11: {
12:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
13:         int sum = 0;
14: 
15:         for_each( x, x+10, closure_object( sum ) );
16:         cout << sum << endl;
17: }
 

 “[&]”를 사용 하므로서 모든 지역변수를 참조를 캡쳐 할 수도 있습니다.

또한 모든 지역변수를 값으로([=]) 디폴트 캡쳐를 사용하면서도 특정 변수만을 참조로 캡쳐 할 수도 있습니다.

 
01: int main()
02: {
03:         int x[10] = { 1,2,3,4,5,6,7,8,9,10};
04:         int low = 3;
05:         int high = 8;
06:         int sum = 0;
07: 
08:         // low ~ high 사이의 합을 구하는 람다 표현식 입니다.
09:         for_each( x, x+10, [=, &sum](int n) { if ( low < n && n <high) sum += n; });
10:         cout << sum << endl;
11: }
 

이번에는 멤버 함수 안에서 람다 표현식을 사용하는 경우를 생각해 봅시다..

 
01: class Test
02: {
03: public:
04:         void foo() const
05:         {
06:                 int x[10] = { 1,2,3,4,5,6,7,8,9,10};
07: 
08:                 // 멤버 data인 base를 캡쳐 하려고 한다. 하지만 error.
09:                 for_each( x, x+10, [base](int n) { cout << base + n << " ";});
10:                 cout << endl;
11:         }
12: 
13:         Test() : base(100) {}
14: private:
15:         int base;
16: };
17: 
18: int main()
19: {
20:         Test t;
21:         t.foo();
22: }
 

이 경우에는 아래처럼 [this], 또는 [=] 를 사용하면 멤버 data 를 캡쳐할 수 있습니다.

 
01: class Test
02: {
03: public:
04:         void foo() const
05:         {
06:                 int x[10] = { 1,2,3,4,5,6,7,8,9,10};
07: 
08:                 for_each( x, x+10, [this](int n) { cout << base + n << " ";}); // 또는 [=]
09:                 cout << endl;
10:         }
11:         Test() : base(100) {}
12: private:
13:         int base;
14: };
 
5. Nullary Lambda

 

파라미터를 갖지 않는 람다 표현식이 만들수 도 있습니다. 이 경우 파라미터 리스트의 ()를 생략할 수도 있습니다. 다음 코드를 살펴 봅시다.

 
01: int main()
02: {
03:         int x[10] = {0};
04:         int y[10] = {0};
05:         int i = 0;
06:         int j = 0;
07: 
08:         generate( x, x+10,[&i]() { return ++i;});
09:         generate( y, y+10,[&j] { return ++j;}); // () 를생략해도된다.
10: 
11:         for_each( x, x+10, [](int n) { cout << n << " ";});
12:         cout << endl;
13: 
14:         for_each( y, y+10, [](int n) { cout << n << " ";});
15:         cout << endl;
16: }

 

 

6. Mutable Lambda

 

클로져 객체의 함수 호출연산자(operator())는 상수 함수로 만들어 집니다. 따라서, 지역 변수를 캡쳐해 사용하는 람다 표현식은 자신의 상태를 변경 할 수 없습니다. 이경우 mutable 키워드를 사용 하면 값으로 캡쳐한 지역변수의 값을 변경할 수 있습니다.

 
01: int main()
02: {
03:         int x[10] = { 1,3,5,7,9,2,4,6,8,10};
04:         int i = 0;
05: 
06:         // error. 값으로캡쳐한지역변수의값을변경할수없다.
07:         for_each( x, x+10, [=](int n) { cout << ++i << " : " << n << endl;});
08: 
09:         // ok.
10:         for_each( x, x+10, [=](int n) mutable { cout << ++i << " : " << n << endl;});
11: }
 
boost에서 지원하던 람다개념을 C++0x에서는 언어 차원에서 지원하므로서 STL을 훨씬 편리 하게 사용할 수가 있게 되었습니다.
 





반응형

+ Recent posts