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가 7과 3 으로 바인딩 되었다. 따라서, closureFunc 는 이미 7 * 5 + 3 의 결과만을 출력하는 closure의 특성을 가지게 되었다.

다르게 표현하자면, closureFunc 함수는 a 와 b 의 값에 "Closed over(Closure)" 되었다고 할 수 있다. 이와 갈은 변수의 바인딩 방식을 static(lexical) binding 이라 부른다.

Closure 특성이 유용한 이유는 위 예제에서도 볼 수 있듯이 late evaluation 이 가능하다는 것이다.

일단 특정 환경에서 함수를 정의한 후 나중에 함수를 호출하여 실행시킬 수 있는 것이다. 이 때 closure 함수는 바뀐 주변 환경과 관계없이 자신이 정의될 때의 환경에 따라 행동한다.

즉, closureFunc 함수는 언제 어디서 호출되더라도 a = 7, b = 3의 값을 보존하고 있는 것이다.

이러한 closure 의 특징은 하나의 환경 하에서 여러 개의 코드 블록을 정의하여, 이후 해당 함수들이 필요할 때 실행시킬 수 있는 이른바 multiple function 의 사용을 가능하게 한다.

8. 정리

  1. lambda는 람다 표현식 또는 람다 함수, 그리고 이름 없는 함수라고 불리며, 함수 오브젝트 중 하나이다.
  2. lambda는 특별한 타입을 가지고 있다고 하지만 decltype 이나 sizeof 는 사용 불가
  3. lambda를 정의한 곳의 외부 변수를 lambda 내부에서 사용하고 싶을 때는 참조 또는 복사로 캡쳐한다.
  4. 클래스에서도 lambda를 사용할 수 있으며, 클래스는 lambda를 friend 함수로 인식한다.
  5. lamba는 closure가 아니다. 단지 lambda를 이용하여 closure의 특성을 구현할 수 있는 것이다.