Software Development/C, C++

DALi code review - C++ 코드리딩, 람다 코드 이해하기

huiyu 2021. 2. 3. 06:00

문제가 와서 보게 된 코드. 해결 패치가 왔지만 이해되지 않는다. 수정 내용은 대략 아래와 같음.

먼저 처음 코드부터 보자.

EraseIf()의 인자로 넘겨주는 값이 이상하다. 이것은 람다 표현식이라고 한다. 대략 이러한 모습

이렇게 4가지 형태로 이루어져 있다. 각각 개시자(introducer), 인자(parameters), 반환타입(return type), 함수의 몸톰(statement)라고 한다.

아 이제 넘겨주는 인자의 형태가 보인다.
 1) [] : 개시자(introducer)
 2) (auto& animator) : 인자(parameters)
 3) -> : 없음, 리턴타입이 void일 경우 생략 가능.
 4) {return animator->Orphan();} : 함수의 몸톰.

좀더 쉬운 형태의 람다 예제를 봐보자. 아래형태이다.
비어있는 [](개시자), ()(인자값). ->역시 void로 생략, 함수의 몸통은 cout을 프린트하고 있다.

[]() {cout << "TestCode" << endl; };

이 람다표현식은 바로 런타임에 실행시켜 볼 수 있다. 이렇게. (끝에 ()를 더해서 호출했다.)

[]() {cout << "TestCode" << endl; }();

실행결과 : TestCode

 이 함수는 실행시 이름은 없지만, 메모리 상에 임시적으로 존재하는 클로져(Clousure) 객체로 생성된다. 이 객체는 함수 객체(function object)처럼 행동하며, 클로져 오브젝트는 임시 객체로 그 줄의 끝에서 파괴된다. (참고 : [C++] 클로져(Closure)가 그래서 무엇인가요)

이제 람다에 인자를 넘겨줘보자. 아래 인자 부분에 (int v)를 넣어 int형 인자를 넘겨준다. 
출력시 이 int v를 6과 곱하여 출력한다.

[](int v) { cout << v << "*6=" << v * 6 << endl; };

마찬가지로 출력해보자.

[](int v) { cout << v << "*6=" << v * 6 << endl; }(7);

7을 인자로 넘겨주고 7*6을 계산한 값이 출력된다.

결과 : 7*6=42

그렇담 내가 본 패치에 추가된 코드를 통해 보면, 아래 형태는 animator를 인자로 넘겨주어 Orphan()을 호출하는 람다식이다.

[](auto& animator) { return animator->Orphan(); }

* Orphan() 함수의 구현부. animator 객체가 유효한 객체인지 판단한다. 이 람다 함수는 실행되며 넘겨지는 animator 객체가 유효한지 판단하는 식이다.

 

다음으로 넘어가기전에 []에 대해 알아보면, 이 부분은 '개시자'로 Capture란 기능을 정의한다.

Caputre란? 람다 안에서 람다 밖에 정의된 변수에도 접근하고 싶을 때 사용. 단순히 인자로 넘겨주는 것보다 더 효율적으로 활용할 수 있다.

 - [&] : 외부의 모든 변수들을 레퍼런스로 가져온다. (Call-by-reference)
 - [=] : 외부의 모든 변수들을 값으로 가져온다.(Call-by-value)
 - [=,&x,&y] or [&, x, y] : 외부의 모든 변수들을 값(or 레퍼런스)로 가져오되, x/y 값만 레퍼런스(or값)으로 가져온다.
 - [x, &y, &z] : 지정한 변수를 지정한 형태로 가져온다.

Caputre 예제

int total_elements = 1;
vector<int> cardinal;

cardinal.push_back(1);
cardinal.push_back(2);
cardinal.push_back(4);
cardinal.push_back(8);

for_each(cardinal.begin(), cardinal.end(), [&](int i) { total_elements *= i; });

cout << "total elements : " << total_elements << endl;

출력결과
total elements : 64

- 예제는 cardinal에 1,2,4,8 값을 넣은 뒤 for_each를 통해 순회하며 total_elements에 값을 곱하고 있다. total_elements는 외부 변수인데 캡쳐를 &로 넘겨주면서 람다 내부에서도 사용할 수 있게 된다.

다시 dali code를 보면,

[](auto& animator) { return animator->Orphan(); }

 위 람다식은 EraseIf()함수로 받고 있다. Eraseif는 template을 통해 class Predicate를 받고 있는데, 클로저 객체 역시 특정 타입의 객체임으로 template을 통해 받을 수 있다.

다시 std::remove_if()코드부터 살펴보자.
remove_if()란 함수는 begin~end까지함수중 funtion에 일치하는 값을 재정렬하는 함수이다.(remove_if이해하기)
remove_if()는 function에 해당하는 값을 모두 뒤로 보낸뒤, 지워야할 값의 시작점을 리턴한다. 그렇기에 위 코드는 Erase()를 통해 반환되는 시작점~ end까지를 모두 삭제하고 있다.

여기서 function에 해당하는 값은 다시 람다식이다. 예제에서 보던 것과는 다르게 길어져서 그렇지, 람다식이다.

//function 함수에 대입하는 람다식
auto function = 
[predicate](auto& obj) {
      if(predicate(obj))
      {
        delete obj;
        return true;
      }
      else
      {
        return false;
      }
    };

auto function = [predicate](auto* obj) { 함수의 본체 } 

-function이라는 함수포인터에 대입하고 있다. 이 함수 포인터를 remove_if에 넘겨준다.
- [predicate] : Capture, 외부에서 선언된 predicate를 람다식 내부에서도 사용할 수 있다. 여기서 predicate는 처음에 봤던 Orphan()을 실행하는 몸통이다.
- (auto& obj) : obj를 인자로 받는 람다식, 이 코드에서 관리하는 객체, animator 객체를 넘겨주게 된다.
- 함수의 본체 : 결국 Orphan()함수를 통해 넘겨지는 animator객체가 유효한지 판단한다. 유효하다면 delete후 true를 리턴하여, remove_if에 대상이 되도록 컨테이너의 맨 뒤로보내게 된다.

 어렵게 느껴졌는데, 구조만 이해해도 코드가 눈에 들어온다. 어렵더라도 차근히 공부해보는게 중요한 거 같다. 

 

아래 링크는 람다를 쉽게 잘 설명하고 있다.
modoocode.com/196#page-heading-0

 

씹어먹는 C++ 토막글 ② - 람다(lambda) 함수

 

modoocode.com

 

728x90