C++ 프로그래밍/함수 Functions

람다 펑션 Lambdas function (anonymous functions)

멜번 아들 셋 개발자 2020. 9. 24. 17:16

람다 펑션 - Lambdas function

람다 펑션은 실무에서 많이 사용되는 것중 하나로 아주 중요 합니다.

아래 예제는 std::stringfind_if()를 통해 string array 에서 "nut"을 포함 하는 string을 찾는 예제 입니다. 

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
 
bool containsNut(std::string str) 
{
	// 매게변수로 받은 str값에서 nut을 발견 하면 True 를 리턴한다.
	return (str.find("nut") != std::string::npos);
}
 
int main()
{
  const std::array<std::string, 4> arr{ "apple", "banana", "walnut", "lemon" };
 
  // std::find_if 의 세번째 인자는 포인터 펑션을 받는다.
  const auto found = std::find_if(arr.begin(), arr.end(), containsNut) ;
 
  if (found == arr.end())
  {
    std::cout << "No nuts\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }
 
  return 0;
}

여기서 문제는  std::find_if()함수 포인터 (function pointer) 를 전달 해야 합니다.  그러기 위해서 펑션을 무조건 정의해야 합니다. 한번의 사용을 위해 펑션을 정의하는것은 유지보수 측면에서 힘들다. 


이러한 문제를 해결 하기 위해 나타난 람다펑션 - Lambdas to the rescue

lambda expression  (람다 또는 클로저라고도 부름)을 사용하면 다른 함수 내에 익명 함수 (anonymous functions)를 정의 할 수 있다. 

람다 구문은 c++ 의 다른 구문과 비교해 봤을때 조금 낯설어서 익숙해 지는데 다소 시간이 걸리지만, 계속적으로 사용을 하다 보면 금방 익숙해 질 수 있다. Lambda는 다음과 같은 형식을 취합니다.

[ captureClause ] ( parameters ) -> returnType
{
    statements;
}

여기서 capture clause parameters 는 생략 될 수 있습니다. 또한 리턴타입 (return type)optional 이며 만약 생략될 경우 auto 타입이 사용됩니다. 마지막으로 람다펑션의 이름 또한 생략 될 수 있습니다. 종합해 보면 람다 펑션은 아래와 같이 구현 할 수 있습니다.

#include <iostream>
 
int main()
{
  []() {}; // 캡쳐, 파라미터와 리턴 타입이 생략 되었다.
 
  return 0;
}

위에 첫번째 예제를 람다 펑션을 이용하여 재 구현 해보자.

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
 
int main()
{
  constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
 
  const auto found{ std::find_if(arr.begin(), arr.end(),
                           [](std::string_view str) // 람다펑션,  no capture clause
                           {
                             return (str.find("nut") != std::string_view::npos);
                           }) };
 
  if (found == arr.end())
  {
    std::cout << "No nuts\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }
 
  return 0;
}

위 예제를 실행하게 되면 첫번째 펑션포인터를 사용한 결과와 동일하다. 


람다 펑션 유형 - Type of a lambda

위 예제에서 람다 펑션을 find_if 함수 안에 정의 하였다. (람다펑션의 시작은 항상 capture clause 으로 시작 되기 때문에[] 보이면, 아!! 이것은 람다펑션이구나!! 생각을 해주면 된다.)  이러한 사용 방식을 함수 리터럴 (function literal) 이라고 한다. 위와 같이 함수 안에 람다를 정의를 하게 되면 코드의 가독성이 떨어질 수 있다. 이러한 문제를 해결 하기 위해 우리는 람다 변수를 정의 한 후 람다 펑션을 사용 할 수 있다.

 

예를 들어,  std::all_of 함수를 사용하여 배열의 모든 요소가 짝수인지 확인합니다. 아래 예제는 std::all_of 펑션 안에 람다를 정의 하였다. 

// 코드의 가독성이 떨어지는 좋지 않은 example
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); });

위 코드는 아래와 같이 바꿀 수 있습니다.

// isEven 변수에 람다를 저장 한다.
auto isEven {
	[](int i) 
    {
    	return ((i % 2) == 0);
    }
}

return std::all_of(array.begin(), array.end(), isEven);

여기서 람다의 타입은 무엇일까? 람다의 타입은 컴파일시 컴파일러가 유니크한 타입을 만들어 준다. 

람다의 타입은 컴파일 타임까지 알 수는 없지만,  람다를 저장하기 위한 방법은 아래와 같이 여러 가지가 있다.

 

Capture clause가 생략되었을 때 함수펑션 (function pointer)를 사용할 수 있다.

// A regular function pointer. Only works with an empty capture clause.
    double (*addNumbers1)(double, double) {
        [](double a, double b) {
            return (a + b);
        }
    };

    std::cout << addNumbers1(1, 2) << std::endl;

Capture clause가 사용될 경우 std::function을 이용하여 람다를 정의 할 수 있다.

// Using std::function.The lambda could have a non - empty capture clause(Next lesson).
int aa = 10;
std::function<int(double, double)> addNumber2;
addNumber2 = [aa](double a, double b) {
	return (aa + a + b);
};

addNumber2(3, 4);

위 예제를 실행시 17이 출력 된다.

 

마지막으로 auto를 사용하여 람다를 정의 할 수 있다.

 // Using auto. Stores the lambda with its real type.
  auto addNumbers3{
    [](double a, double b) {
      return (a + b);
    }
  };
 
  addNumbers3(5, 6);

람다의 실제 유형을 사용하는 유일한 방법은 auto keyword을 사용하는 것입니다. autostd::function에 비해 오버 헤드가 없다는 이점도 있습니다.

 

하지만 람다를 정의할때 항상 auto 을 사용 할 수 있는 것은 아니다. 예를 들어 람다를 매개변수로 함수에 전달하고 호출자가 전달하는 람다의 타입을 결정 하기 때문에 auto를 사용 할 수 없다. 이러한 경우엔 std::function을 사용하여 정의 할 수 있다. 

#include <functional>
#include <iostream>
 
// We don't know what fn will be. std::function works with regular functions and lambdas.
void repeat(int repetitions, const std::function<void(int)>& fn)
{
  for (int i = 0; i < repetitions; ++i)
  {
    fn(i);
  }
}
 
int main()
{
  repeat(3, [](int i) {
    std::cout << i << '\n';
  });
 
  return 0;
}

위 예제는 다시 아래와 같이 바꿀 수 있다. using 키워드와 std::function 의 예는 melbourne-engineer.tistory.com/3?category=807213 


using printI = const std::function<void(int)>;
void repeat(int repetitions, printI fn) {
    for (int i = 0; i < repetitions; i++) {
        fn(i);
    }
}

int main()
{
  repeat(3, [](int i) {
    std::cout << i << '\n';
  });
 
  return 0;
}

 

 

이포스트의 원문은 www.learncpp.com/cpp-tutorial/introduction-to-lambdas-anonymous-functions/