토막글 2. 람다(lambda)
서론
어떤 벡터의 원소들의 모든 곱을 계산하는 코드를 생각해보자 -> 아래 예제는 반복자를 이용해 cardinal 의 각 원소들을 순차적으로 곱해나가는 코드이다.
아래와 같이 함수를 이용해 변형할 수도 있다. -> for_each 를 사용해 이전 코드의 while 부분을 모두 없앨 수 있지만, 이를 위해 필요한 함수를 구성하는 코드가 훨씬 길다. (배보다 배꼽이 크다..)
물론 전체적인 코드의 질이 높아졌다고 볼 수 있지만, 함수를 이용하기 위해서 product 라는 구조체를 생성하면서 생성자를 만들고, 또 void operator() 도 정의해주어야 한다. (상당히 귀찮은 일이다..)
아래 코드는 람다식을 사용한 예제로 while, 구조체와 같은 코드 없이 for_each의 특성을 그대로 잘 살려주었다.
람다(lambda) 의 구성
람다의 구성
개시자 [] : 그 안에 어떤 외부 변수를 써 넣는다면 람다 함수가 이를 캡처해서, 이 변수를 람다 내부에서 이용할 수 있게 된다. -> 값에 의한 캡처 : 값복사가 이루어진다. -> 참조에 의한 캡처 : lvalue reference 로 외부 변수에 영향을 줄 수 있다.
매개변수 () : 람다가 실행시 받을 인자들을 넣는다.
리턴타입 -> : 리턴타입을 넣는다. (리턴타입을 명시하지 않을 시 void 리턴타입이다)
내용 {} : 람다가 실행하는 내용들을 넣는다.
아래 코드에서는 런타임시 이름은 없지만, 메모리 상에 임시적으로 존재하는 클로저 개체(closure object) 가 생성된다. -> 이 클로저 개체는 함수 개체(function object) 처럼 행동한다.
아래와 같이 캡처, 매개변수, 리턴타입을 모두 사용하지 않는 람다는 생각할 것도 없이 내용만 실행된다.
조금더 복잡한 아래의 예제는 v 를 매개변수로 사용하는 람다식에 7을 전달하여 42를 출력하는 결과를 확인할 수 있다.
또한 매개변수가 없을 때 아래 두 람다식은 동일한 의미이다.
캡처(capture)
많은 경우, 우리는 람다 않에서 람다 밖에 있는 변수들에 접근하고 싶을 때가 있다. 이때 사용할 수 있는 방법으로 C++ 은 캡처를 제공하고, 다음의 4가지 유형이 있다.
[&]() { /* */ }
외부의 모든 변수들을 레퍼런스로 가져온다. (함수의Call - by - reference
를 생각)[=]() { /* */ }
외부의 모든 변수들을 값으로 가져온다. (함수의Call - by - value
를 생각)[=, &x, &y] { /* */ }
, 혹은[&, x, y] { /* */ }
외부의 모든 변수들을 값/레퍼런스로 가져오되,x
와y
만 레퍼런스/값으로 가져온다[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