Visual C++ 2010의 새기능 – 람다 표현식(Lambda Expression)

Visual Studio 2010이 발표된지 꽤 오랜 시간이 지났는데 이제야 이런 포스팅을 올립니다.

Visual C++ 2010은 새로운 C++ 표준안이 나오기 전 거의 확정된 일부 기능을 포함시켰는데요.
그중 하나가 람다 표현식(Lambda Expression) 입니다.
그럼 그 람다 표현식에 대해 알아 보겠습니다.

1. 익명 함수
람다 표현식은 익명 함수 오브젝트를 만드는 문법으로,
익명 함수라는 것은 함수의 본문은 있으나 이름이 없는 것을 말합니다.

2. 함수 오브젝트(Function Object 또는 Functor)
C에서의 함수는 어떤 명령이 정의되어 있는 포인터로 해석할 수 있습니다.
이 포인터는 대부분 컴파일과 링크 과정에서 이름과 연결되게 되어 있습니다.
필요한 경우 함수 포인터 변수에 런타임에 지정하여 사용하기도 했습니다.
예를 들면 Windows API에서 사용하는 수많은 콜백 함수들이 있죠.
C++ 에서는 이런 함수의 기능에 좀 더 기능을 추가한 함수 오브젝트를 제공하였습니다.
함수 오브젝트는 단순한 함수에 현재의 상태를 저장할 수 있는 것이 큰 차이점 입니다.

3. 함수, 함수 오브젝트, 람다 표현식
모두 함수라는 공통점을 가지고 있습니다.
함수는 문법 오버헤드가 적은 대신에 상태를 저장할 수 없습니다.
함수 오브젝트는 상태 저장은 가능하지만 클래스 정의에 따른 문법 오버헤드가 필요합니다.
람다 표현식은 함수 오브젝트처럼 상태 저장이 가능하지만 클래스 정의를 위한 문법 오버헤드는 없습니다.
따라서 함수 오브젝트를 사용하는 많은 코드에서 기존 함수 오브젝트보다 람다 표현식이 더 깔끔하게 코드를 작성할 수 있습니다.

4. 문법 소개
람다 표현식은
[<캡처>](<파라미터>) <mutable 정의> <throw 정의> <return 정의> { <함수 본문> }
으로 정의됩니다. 이제 하나씩 알아 보겠습니다.

4.1. 캡처
람다 표현식은 정적 변수에는 언제나 접근이 가능합니다. 이는 일반 함수와 동일하죠.
그외 변수에 접근하기 위해 컴파일러에게 알려주는 표현이 캡처입니다.
캡처에는 복사 캡처와 참조 캡처가 있습니다.
[]: 어떤 변수도 캡처하지 않습니다.
[=]: 람다 외부 변수 모두 복사 캡처, 클래스 멤버는 참조 캡처
[&]: 람도 외부 변수 모두 참조 캡처, 클래스 멤버는 참조 캡처
[this]: 클래스 멤버만 참조 캡처
[var]: 람다 외부의 var 변수를 복사 캡처
[&var]: 람다 외부의 var 변수를 참조 캡처
[=, &var]: 람다 외부의 var 변수는 참조 캡처, 그외 변수는 복사 캡처
[&, var]: 람다 외부의 var 변수는 복사 캡처, 그외 변수는 참조 캡처

4.2 파라미터
파라미터 정의는 일반 함수와 거의 유사합니다. 단 아래와 같은 것은 불가능 합니다.
기본값 정의 불가능: [](int n = 10) {} (X)
가변 길이 파라미터 불가능: [](const char* fmt, …) {} (X)
이름 없는 파라미터 불가능: [](int) {} (X)

4.3. mutable 정의
람다 표현식의 함수 본문 안에서 복사 캡처한 변수의 값은 수정할 수 없습니다.
복사 캡처한 변수는 마치 const 와 같이 인식하는 것이죠.
const 멤버 함수에서 멤버 변수 값을 수정하듯이 mutable 키워드를 명시하면 복사 캡처된 변수의 값을 수정할 수 있습니다.
[=var]() { var += 1; } (X)
[=var]() mutable { var += 1; } (O)

4.4. throw 정의
C/C++의 throw 정의와 똑같습니다.
throw() 하면 함수에서 예외를 throw 하지 않는다가 되는 것이고
throw(…) 하면 뭔가를 throw 한다가 됩니다.
Visual C++에서는 저 두가지만 유효합니다.
throw(Exception) 뭐 이런 것은 throw(…)하고 동일하게 인식합니다.
throw 정의는 컴파일 옵션의 예외 처리 옵션과 연관이 있사오니 관련 문서를 참조하세요.

4.5. return 정의
생략하게 되면 void라고 생각하세요.
물론 생략하면서 특정 값을 반환할 수도 있습니다. 예를 들면
[](int n) { return (n < 10); }
와 같이 함수 본문이 달랑 return 하나로만 되어 있을 경우 컴파일 타임에 해당 반환값을 판단하여 return 타입을 정해 줍니다.
하지만… 코드를 읽는 입장에서 return을 정의하지 않고 사용한다면 헷갈리고 좋지 않을 수 있으니 반환을 사용하는 람다 표현식에서는 꼭 명시하세요.
위 예제에서 return 을 정의한다면
[](int n) -> bool { return (n < 10); }
이 되겠습니다.
-> 반환 타입
이렇게 되는 것인데 반환 타입은 void를 제외한 데이터 타입입니다.

4.6. 함수 본문
일반 함수의 본문 정의와 다를 바가 없습니다.
재귀 호출도 가능합니다.

5. 예제: 람다 표현식과 함수 오브젝트 코드 비교
아래 예제를 통해 람다 표현식과 기존 함수 오브젝트의 코드를 비교해 보세요.

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int usingLambda(vector<int>& v)
{
    int ret = 0;
    std::for_each(v.begin(), v.end(), [&ret](int n) {
        if (n < 10)
            ret += n;
    });
    return ret;
}

int usingFunctionObject(vector<int>& v)
{
    struct Functor
    {
        Functor(vector<int>& v, int& ret) : v(v), ret(ret) {}
        void operator()(int n)
        {
            if (n < 10)
                ret += n;
        }
        vector<int>& v;
        int& ret;
    };

    int ret = 0;
    std::for_each(v.begin(), v.end(), Functor(v, ret));
    return ret;
}

int main(void)
{
    vector<int> v;
    for (int i = 1; i <= 20; ++i)
        v.push_back(i);

    cout << usingLambda(v) << endl;
    cout << usingFunctionObject(v) << endl;

    return 0;
}

6. 예제: 람다 표현식 예제

#include <iostream>

using namespace std;

int gVar = 0;

class A
{
public:
    A() : n(10) {}
    void f1()
    {
        // this를 캡처했지만 gVar로 변경됩니다.
        [this](){ ++n; cout << n << endl << gVar++ << endl; }();
    }
    void f2()
    {
        // = 로 복사 캡처 했지만 클래스 멤버 n의 값이 변경됩니다.
        [=]() { ++n; cout << n << endl << gVar++ << endl; }();
    }
    void g()
    {
        cout << n << endl;
        // 아무것도 캡처하지 않았지만 gVar를 참조할 수 있습니다.
        [](){ cout << gVar << endl; }();
        // 단일 반환문으로 된 본문은 반환 타입 정의를 생략할 수 있습니다.,
        bool b = [](int n) { return (n < 10); }(10);
        cout << b << endl;
    }
private:
    int n;
};

int main(void)
{
    A a;
    a.f1();
    a.f2();
    a.g();


    /*
        output:
            11
            0
            12
            1
            12
            2
            0
    */
    return 0;
}