토막글 2. 람다(lambda)

서론

  • 어떤 벡터의 원소들의 모든 곱을 계산하는 코드를 생각해보자 -> 아래 예제는 반복자를 이용해 cardinal 의 각 원소들을 순차적으로 곱해나가는 코드이다.

vector<int>::const_iterator iter = cardinal.begin();
vector<int>::const_iterator iter_end = cardinal_end();
int total_elements = 1;
while(iter != iter_end)
{
    total_element *= *iter;
    ++iter;
}
  • 아래와 같이 함수를 이용해 변형할 수도 있다. -> for_each 를 사용해 이전 코드의 while 부분을 모두 없앨 수 있지만, 이를 위해 필요한 함수를 구성하는 코드가 훨씬 길다. (배보다 배꼽이 크다..)

  • 물론 전체적인 코드의 질이 높아졌다고 볼 수 있지만, 함수를 이용하기 위해서 product 라는 구조체를 생성하면서 생성자를 만들고, 또 void operator() 도 정의해주어야 한다. (상당히 귀찮은 일이다..)

int total_elements = 1;
for_each(cardinal.begin(), cardinal.end(), product<int>(total_element));
template <typename T>
struct product 
{
    product(T& storage) : value(storage) {}
    template <typename V>
    void operator()(V& v) {
      value *= v;
    }
    T& value;   
}
  • 아래 코드는 람다식을 사용한 예제로 while, 구조체와 같은 코드 없이 for_each의 특성을 그대로 잘 살려주었다.

람다(lambda) 의 구성

  • 람다의 구성

    • 개시자 [] : 그 안에 어떤 외부 변수를 써 넣는다면 람다 함수가 이를 캡처해서, 이 변수를 람다 내부에서 이용할 수 있게 된다. -> 값에 의한 캡처 : 값복사가 이루어진다. -> 참조에 의한 캡처 : lvalue reference 로 외부 변수에 영향을 줄 수 있다.

    • 매개변수 () : 람다가 실행시 받을 인자들을 넣는다.

    • 리턴타입 -> : 리턴타입을 넣는다. (리턴타입을 명시하지 않을 시 void 리턴타입이다)

    • 내용 {} : 람다가 실행하는 내용들을 넣는다.

  • 아래 코드에서는 런타임시 이름은 없지만, 메모리 상에 임시적으로 존재하는 클로저 개체(closure object) 가 생성된다. -> 이 클로저 개체는 함수 개체(function object) 처럼 행동한다.

  • 아래와 같이 캡처, 매개변수, 리턴타입을 모두 사용하지 않는 람다는 생각할 것도 없이 내용만 실행된다.

  • 조금더 복잡한 아래의 예제는 v 를 매개변수로 사용하는 람다식에 7을 전달하여 42를 출력하는 결과를 확인할 수 있다.

  • 또한 매개변수가 없을 때 아래 두 람다식은 동일한 의미이다.

캡처(capture)

  • 많은 경우, 우리는 람다 않에서 람다 밖에 있는 변수들에 접근하고 싶을 때가 있다. 이때 사용할 수 있는 방법으로 C++ 은 캡처를 제공하고, 다음의 4가지 유형이 있다.

    1. [&]() { /* */ } 외부의 모든 변수들을 레퍼런스로 가져온다. (함수의 Call - by - reference 를 생각)

    2. [=]() { /* */ } 외부의 모든 변수들을 값으로 가져온다. (함수의 Call - by - value 를 생각)

    3. [=, &x, &y] { /* */ }, 혹은 [&, x, y] { /* */ } 외부의 모든 변수들을 값/레퍼런스로 가져오되, xy 만 레퍼런스/값으로 가져온다

    4. [x, &y, &z] { /* */ } 지정한 변수들을 지정한 바에 따라 가져온다.

예제1. 참조에 의한 캡처1

예제2. 참조에 의한 캡처2

  • 아래 코드에서 클로저 개체는 특정 타입의 리턴하기 때문에, template 에서 typename T 로 받을 수 있다. -> fill 함수는 특정 타입 T 의 변수 done 으로 클로저 개체를 받았다.

  • 이 때, 클로져 개체 자체는 이미 stuff 를 캡처해서 stuff 에 대한 레퍼런스를 계속 가지고 있는 상태이고, fill 의 while 문을 돌면서 stuff 의 크기가 8 이상일 때까지 수행하게 된다.

예제3. 참조에 의한 캡처3

예제4. 값에 의한 캡처의 시점

  • 클로저 개체가 값을 캡처하는 시점은 func 가 정의될 때 캡처한다. -> 실행할 때 캡처하는 것이 아니다.

예제5. 값에 의한 캡처

  • 값에 의한 캡처는 자동적으로 const 속성이 붙기 때문에, 값으로 캡처한 그 변수들의 내용을 바꿀 수 없다.

  • 만약 값으로 캡처한 상황에서 변수의 값을 바꾸고 싶다면 mutable 키워드를 사용하자.

캡처의 범위

  • 캡처 되는 개체들은 모두 람다가 정의된 위치에서 접근 가능해야만 하다.

  • 아래 코드는 i,j 모드 접근이 가능하기 때문에, 정상정으로 실행이 된다.

  • 이레 코드 또한 바깥의 람다에서 i 를 캡처하였기 때문에, 내부 람다에서 i 를 캡처할 수 있게 된다.

  • 하지만, 만약 바깥의 람다에서 아무것도 캡처하지 않았다면, 아래의 코드는 당연히 컴파일 에러가 발생한다.

  • 또 다른 예를 살펴보자. 아래의 코드는 바깥의 람다에서는 값을 캡처했는데, 안쪽 람다에서 참조를 사용하고자 하는 경우이다. 이 경우 당연하게 컴파일 에러가 발생한다.

  • 이를 해결하기 위해서 아래와 같이 mutable 키워드를 붙여주면 된다.

클로저 개체(closure object) 의 복사 생성자와 소멸자

  • 모든 클로저 개체들은 암묵적으로 정의된 복사 생성자와, 소멸자를 가지고 있다. -> 클로저 개체가 생성될 때 값으로 캡처 된 것들의 복사 생성이 일어날 것이다.

  • 일단 아래와 같이 trace 개체를 정의해놓고 람다를 통해 클로저 개체를 생성해보자.

  • 다음의 경우 기본 생성자와 소멸자가 호출된다. -> f 에서 t 를 사용하지 않았기 때문에, t 를 캡처하지 않는다.

  • 아래의 예제는 m1 을 생성하면서 람다에서 t 를 사용하기 때문에, t 의 복사 생성자가 호출되게 된다. -> 왜냐면, 값으로 받았기 때문이다! 만약 레퍼런스로 받았다면 복사 생성자가 호출되지 않았을 것이다.

  • 그리고 아래와 같이 auto m2 = m1; 에서 클로저 개체의 복사가 이루어지는데, 이 때 클로저 개체의 복사 생성자가 값으로 캡처된 개체들을 똑같이 복사해주게 된다. -> 따라서, 또 한번 t 의 복사 생성자가 호출된다.

람다의 전달 및 저장

  • 람다를 저장하는 방법으로 다음 두가지 방법을 사용할 수 있다.

  • C++ 11 에서는 클로저 개체를 전달하고 또 저장할 수 있는 막강한 기능이 제공된다. (std::function) -> 그 어떤 클로저 개체나 함수 등을 모두 보관할 수 있는 만능 저장소이다.

  • 아래와 같이 std::function<반환타입(매개변수)> 의 형태로 사용하면 된다.

Last updated