Lambda
: scope 내에 있는 변수에 접근하여 사용할 수 있는 이름없는 함수 객체(Closure)를 생성하는 방법
먼저 함수 객체가 어떤 개념을 갖는지 살펴보자.
p는 객체인데, 마치 function 처럼 쓰이고 있다.
원리는 p.operator()(1, 2) 를 호출하는 것이다.
괄호()도 재정의가 가능하다!
함수 객체 개념
: () 연산자를 재정의 해서 함수 처럼 사용 가능한 객체(Function operator, Functor)
함수 객체를 왜 사용하는 것일까?
cmp1, cmp2 는 함수 이름은 다르지만 signature 가 같기 때문에
함수 포인터 f1으로 가리킬 수 있다.
하지만 signature 가 같기 때문에 cmp1, cmp2 는 같은 type이라고 할 수 있다.
반면, Less, Greater 의 () 는 모두 같은 signature 를 같지만
Less() 는 Less 만의 typedㅣ고, Greater() 는 Greater 만에 종속된 type 이다.
핵심 1. 일반 함수는 자신만의 타입이 없다.
signature가 동일한 함수는 모두 같은 타입니다.
핵심 2. 함수 객체는 자신만의 타입이 있다.
signature 가 동일해도 모든 함수 객체는 다른 타입이 된다.
inline 사용이 필요한 경우를 살펴보자.
먼저 Sort() 함수를 살펴보자.
우리는 정렬 함수를 구현할 때 비교 함수를 함수 포인터로 받아 상황에 따라 동적으로 바꿀 수 있다.
하지만, 아무리 비교 함수(e.g, cmp1, cmp2) 가 inline 으로 선언되었다 해도
실제로 컴파일을 할 때 inline 치환이 되지 않는다.
cmp 로 올 수 있는 것은 무궁무진하기 때문에 컴파일러 입장에서는 inline 으로 치환 할 수 가 없다.
Sort에 인자로 오는 *cmp 함수 포인터는 절대로 inline 치환 될 수 없다.
결국 inline 을 사용하여 성능 향상을 가져올 수 없다.
반면에, 함수 객체를 사용한 Sort2 함수를 보면
컴파일 입장에서 Sort2에 전달되는 less 는 Less type으로 정확히 알 수 있고,
Less 내부에 () 는 inline 으로 되어 있으므로, inline 으로 치환되어 성능향상을 가져올 수 있다.
하지만 Sort2 는 정책을 바꿀 수 없으므로, Greater type을 받아들일 수 없다.
결국 Template 을 사용하면 정책도 바꿀 수 있도록 해야 한다.
Sort3의 T 는 template 이기 때문에, 전달되는 T type 에 따라서 목적 코드가 커지는 단점이 있다.
하지만 이 Sort3() 함수가 아주 큰 function 은 아니고, 전달되는 T type의 개수가 실제 코드에 아주 많지 않다면 큰 문제가 되지 않는다.
실제로 함수 포인터와 함수 객체를 사용했을 때는 비교하면 꽤나 큰 성능 향상을 가져온다.
간단한 예제 프로그램에서만도 10배 정도의 차이를 나타낸다.
그래서 C++11에서는 이 함수 객체를 더 편하게 사용하기 위해 lambda 를 도입한다.
먼저 () 연산자를 재정의할 때 주의할 점이 있는데,
대부분 함수객체의 () 연산자 함수는 상수 함수로 만드는 것이 좋다.
위 예제의 foo() 함수에서 처럼 상수 객체에서 호출하는 경우가 있기 때문인데,
일반적으로 template 함수 작성시 T type을 인자로 받을 때 값 복사가 이뤄지는 redundant 한 상황을
방지하기 위해 관습적으로 const T& 로 선언하게 된다.
위 예제를 보면 결국 const T& a 가 되는데, a는 상수 객체가 된다.
상수 객체는 상수 함수만을 호출할 수 있으므로 () 연산자 재정의시에 상수 함수로 만들어야 한다.
그럼 이제 Lambda 표현식을 어떻게 사용하는지 살펴보자!
람다 표현식(Lambda Expression)은 함수 객체를 만드는 표현식이다.
[] 는 Lambda Introducer 라고 하며, Lambda가 시작됨을 알리는 표현식이다.
위 sort() 코드는 컴파일러가 아래와 같이 함수 객체를 생성하는 코드로 변경하게 된다.
Closure(클로져)는 람다 표현식을 통해서 컴파일러가 만든 클래스이다.
결국 Lambda 를 사용하는 이유?
함수객체를 사용해서 inline 치환을 하고 성능 향상을 얻기 위해서이다.
Lambda의 특징에 대해 살펴보자.
f1, f2 는 구현이 완전히 똑같은 객체다.
하지만 람다는 함수를 만드는게 아니라 함수 객체를 만드는 것으로
결국 f1, f2 는 다른 타입이 된다.
실제로 f1, f2 의 이름이 다른 것을 RTTI 기법을 사용하여 확인할 수 있다.
결론 : 모든 람다는 다른 타입이다.(함수 객체 이므로..)
위 예제에서와 같이 다시 f1에 함수 객체를 넣으려고 하면,
실제 구현 코드는 똑같아도 위와 다른 타입이기 때문에
f1에 다른 타입을 넣을 수 없어서 컴파일 에러가 난다.
앞으로 f1은 다른 타입을 가리킬 수 없게 된다. 그러므로 inline 치환된다.
Labmda 는 함수 포인터로의 변환도 가능하다.
하지만 f2 는 함수 포인터 변수 이므로, 상수가 아니다.
이후 code에서 다른 함수를 가리킬 수 있으므로,
그러므로 inline 치환되지 않는다.
f3도 다른 function 을 참조할 수 있다. e.g, f3 = &foo;
결국엔 inline 치환되는 것은 오직 f1하나뿐이다.
inline 치환되기 위해서는 컴파일러가 해당 타입이 변경되지 않을 것임을 명확하게 인지되어야 한다.
따라서 아무리 Labmda 표현식을 사용하더라도 그것을 담을 변수가 변경되지 않는 속성을 가져야 한다.
Lambda 에 전달될 수 있는 인자로는 무엇이 좋을지 다시 정리해서 생각해보자.
1. 함수 포인터
- 가능하긴 하지만 함수 포인터는 inline 치환이 되지 않으므로, lambda 를 사용하는 의미가 없어진다.
2. function template class
- 가능하지만 function<> 으로 전달되는 것은 변수이므로 inline 치환되지 않는다.
3. auto
- auto 는 절대 함수 인자로 받을 수 없다.
4. template
- OK!! inline 치환이 된다.
결국, 람다 표현식을 인자로 받으려면 template 을 사용하자!
Lambda에서 return type은 어떻게 표시해야 할까?
f1의 경우 return type이 int 하나로 결정되어 있는 경우, 컴파일러가 자동으로 추론한다.
하지만, f2처럼 -> 를 사용해서 람다의 리턴 타입을 명확하게 표시할 수 도 있다.
f3처럼 if-else 문이 있더라도 반환하는 타입이 동일하기 때문에, 컴파일러가 리턴 타입을 자동으로 추론할 수 있다.
하지만 f4와 같이 2개 이상의 리턴문이 다른 타입을 반환하면
반드시 trailing return 을 표시해야 한다.
Lambda 내부에서 Lambda외부에 선언된 변수에 접근(Capture)하기 위한 방법은 무엇일까?
Lambda 외부에 선언된 변수에 접근하고자 할 때는 [] 안에 해당 변수이름을 전달한다.
또한 해당 block 내의 모든 지역 변수에 접근하고자 할 때는 "="를 전달한다.
실제로 []에 변수 이름을 전달하거나 "="를 전달했을 때 생성되는 코드는 다음과 같다.
결국엔 Closure 객체 안에 멤버 변수로 보관하고 있는 것이다.
Closure에서 변수를 capture하고 있는 원리에 한 단계 더 접근해보자.
v1 = 0; 은 에러를 발생시킨다.
() 연산자를 재정의한 것이 상수 함수이므로, 멤버 변수를 변경할 수 없기 때문에 컴파일 에러가 발생한다.
그렇다면 Closure_Object 내에 멤버 변수에 mutable 선언하면 되지 않을까?
mutable 키워드를 사용하여 mutable lambda 로 선언한다.
지역변수를 캡쳐한 멤버 변수가 mutable 이 된다.
하지만 Closure_Object의 v1 멤버 변수(복사본)이 변경된 것이지
main 함수의 v1 지역변수(원본)이 변경된 것이 아니다.
main 함수의 지역변수(원본)을 변경하려면 mutable 키워드는 삭제하고, & 를 추가한다.
[]에 &를 전달했을 때 컴파일러가 생성하는 Closure 객체는 다음과 같다.
지역 변수가 아닌 class 의 멤버 변수에 접근하기 위해서는 어떻게 해야할까?
단순히 [] 에 data를 전달하면 되지 않을까 생각할 수 있지만,
data 는 지역변수가 아니기 때문에 [data]라고 쓸 수 없다.
Class내에서 내부 어디서든 접근 가능한 키워드를 생각해보면, this 가 있다!
[]에 this를 전달하면 &키워드가 없으므로, 값이 전달된다.
하지만 this 는 포인터이기 때문에 멤버변수 data의 변경이 가능하다.
결국엔 컴파일러에 의해 생성되는 Closure_Object 멤버 변수로 Test* 가 있는 것이다.
또한 또한 this외에도 "=" 로도 가능하다!
인자가 없는 Lambda 를 표현하려고 한다면?
인자가 없는 경우에는 ()를 생략해도 된다.
피드 구독하기:
댓글 (Atom)
[C++] meta programing
재귀 호출에 관해 template meta programming 을 적용한 예제를 살펴보자. #include using namespace std; int fact(int n){ if(n factorial 연산을 하는 일반적인 재귀 호출 함...
-
Smart Pointer : 포인터처럼 동작하며 자동으로 메모리를 해제하고 안전하게 resource를 관리하도록 돕는 객체 포인터는 소멸자가 호출되지 않아 memory leak이 발생한다. Java, C#같은 VM이 있는 언어는 VM에서 G...
-
nullptr 란? C++11 에서 지원하는 null pointer 상수 먼저 pointer 가 초기화될 수 있는 정수 값을 살펴보자. 정수 0은 모든 타입의 포인터에 암시적 형변환을 통해서 초기화 값으로 사용될 수 있다. 하지만 그 이...
-
자료 구조를 순회하여 데이터에 접근하는 방법을 살펴보자. Container 에 저장된 데이터에 접근하기 위해 대표적으로 Iterator의 begin(), end() function 을 사용한다. 하지만 위 예제에서 show() 함수에 배열이...
댓글 없음:
댓글 쓰기