람다 펑션 - 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/

'C++ 프로그래밍 > 함수 Functions' 카테고리의 다른 글

함수 포인터 Function Pointers - 1  (0) 2020.09.24

함수 포인터 - Function Pointer

함수 포인터 ( Function Pointer ) 에서 가장 헷갈리는 부분은 아마도 Syntax 일 것입니다. 함수 포인터의 Syntax 는 아래와 같습니다.

data-type (*pointerName)(parameters);

1. int (*fcnPtr)(int, std::string);

2. void (*fcnPtr)();

3. bool (*fcnPtr)(std::string);

첫번째로 데이터 타입이 나오며, 그 뒤로 함수 포인터의 이름, 그리고 포인터를 가르키는 * 이 함수 포인터 이름 앞에 붙습니다. 그리고 함수 포인터가 가르키는 함수의 parameter 타입을 정의 합니다. 

 

예를들면 

1. 함수의 리턴형은 int 여야 하고, int 값과 std:string 2개의 파라미터를 받는 함수여야 한다

2. 함수의 리턴형은 void, 파라미터는 받지 않는 함수여야 한다.

3. 함수의 리턴형은 bool 이며, std::string 을 파라미터로 받는 함수여야 한다.

 

함수 포인터에 가장 빠르게 익숙해 지는 방밥은 계속 사용하는 것입니다. 간단한 void 함수를 만든 후 void 함수를 함수 포인터를 할당 후, 함수 포인터를 사용하여 할당된 void 함수를 호출해 보도록 하겠습니다.

#include <iostream>

void firstFunction() {
	std::cout << "first function without any paramter" << std::endl;
}

int secondFunction(int a, int b) {
	return a + b;
}

bool thirdFunction(std::string name) {
	return !name.empty();
}


int main() {
	void (*firstPtr)() = firstFunction; 
    // 리턴타입이 void이며, parameter를 받지 않는 포인터 함수 선언; 선언과 동시에 firstFunction의 주소값을 포인터함수에 할당
	firstPtr();

	int(*secondPtr)(int, int) = secondFunction; 
    // 리턴타입이 int이며, 두개의 int 값을 파라미터로 받는 포인터 함수 선언; 선언과 동시에 secondFunction의 주소값을 포인터 함수에 할당
	std::cout << secondPtr(10, 20) << std::endl;

	bool (*thirdPtr)(std::string) = thirdFunction;
	if (thirdFunction("name")) {
		std::cout << "return value is true" << std::endl;
	}
}

 

함수를 다른 함수의 파라미터로 전달. (Callback Function)Passing functions as arguments to other functions

 

함수 포인터는 다른 함수의 파라미터로 전달 될 수 있으며, 흔히 콜백펑션이라고 부른다. 여기서 중요한 포인트는 함수를 전달 할때 정의되어져 있는 함수포인터의 리턴타입, 파라미터의 값일 정확하게 일치 하여야 한다. 

#include <utility> // for std::swap
#include <iostream>

// 세번째 인자는 포인터 함수로, bool 값을 리턴 하며, 두개의 int 값을 받는 comparisonFcn
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
{
    for (int i = 0; i < (size - 1); ++i)
    {
        int bestIndex = i;

        for (int currentIndex = i + 1; currentIndex < size; ++currentIndex)
        {
            // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
            if (comparisonFcn(array[bestIndex], array[currentIndex]))
            {
                bestIndex = currentIndex;
            }
        }

        std::swap(array[i], array[bestIndex]);
    }
}

// 오름차순 정렬 함수. X 와 Y 의 값을 비교하여 X 가 Y 보다 클 경우 True를 작을 경우 False를 반환 한다.
bool ascending(int x, int y)
{
    return x > y;
}

// 내림차순 정렬 함수. X 와 Y 의 값을 비교하여 X 가 Y 보다 작을 경우 True를 클 경우 False를 반환 한다.
bool descending(int x, int y)
{
    return x < y; // swap if the second element is greater than the first
}

// 결과 출력 하는 함수
void printArray(int* array, int size)
{
    for (int index{ 0 }; index < size; ++index)
    {
        std::cout << array[index] << ' ';
    }

    std::cout << '\n';
}

int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };

    // descending 함수를 3번재 파라미터로 전달. 
    // selectionSort 함수는 파라미터로 int 타입의 Array, int 값과 , bool (*comparisonFcn)(int, int) 함수 포인터를 받는다.
    selectionSort(array, 9, descending);
    printArray(array, 9);

    // ascending 함수를 3번재 파라미터로 전달. 
    // selectionSort 함수는 파라미터로 int 타입의 Array, int 값과 , bool (*comparisonFcn)(int, int) 함수 포인터를 받는다.
    selectionSort(array, 9, ascending);
    printArray(array, 9);

    return 0;
}

예제에서 보는것과 같이 ascending 함수와 descending 함수를 selectionSort 펑션의 파라미터 값으로 전달을 하여 selectionSort 함수 안에서 ascending과 descending를 호출 하여 사용하는 것을 볼 수 있다.  

 

그렇다면 왜? 이렇게 사용을 하는것일까? 함수 포인터를 사용하게 되면 우리는 다양한 함수를 selectionSort 함수 안에서 콜백펑션으로 사용을 할 수 있다. But 함수의 리턴 타입과 정의되어져 있는 파라미터 값이 일치 해야 한다. 

 

위에서 언급한것과 같이 함수 포인터의 문법은 data-type (*fcnName)(parameters) 이다. 위에 예제에서 보는것과 같이 콜백펑션을 사용 하기 위에 bool (*comparisonFcn)(int, int) 를 파라미터로 정의 하였다. 만약 bool (*comparisonFcn)(int, int) 함수포인터를 전반적인 프로젝트 내에서 사용을 한다고 가정을 하면 코드의 가독성이 떨어진다.

 

이 문제를 해결 하기 위해서 우리는 type aliases를 사용 할 수 있다.

 

bool (*comparisonFcn)(int, int); 함수 포인터는 using comparisonFcn = bool(*)(int, int); 로 바꿀 수 있다.

comparisionFcn은 리턴 타입이 bool 이면서, 두개의 int 파라미터를 가지고 있는 함수를 포인팅 할 수 있다. 

 

Type aliase를 사용하여 우리는 위 예제를 아래와 같이 바꿀 수 있다. 

using comparisonFcn = bool(*)(int, int); 
void selectionSort(int* array, int size, comparisonFcn compare)

 // 이 부분도 comparisonFcn 이 아닌 compare로 바꿔 주어야 한다
 // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
 if (compare(array[bestIndex], array[currentIndex]))
 {
 	bestIndex = currentIndex;
 }

C++ 11부터 <functional> standard library 에 정의되어져 있는 std::function 을 사용하여 함수 포인터를 정의 할 수 있다.

#include <functional>
bool validate(int x, int y, std::function<bool(int, int)> fcn); 
// std::function 펑션, 리턴 타입은 bool 이며, 두개의 int값을 파라미터로 받고 있다

위에 예문과 같이 < 리턴타입 ( 파라미터1, 파라미터2) > 정의되어져 있다. 만약 파라미터가 없고 bool 값만 리턴 한다면 std::function<bool () > 와같이 정의를 하면 된다. 

std::function<void(const std::string&)> mErrorHandler;
// 리턴 값은 void 이며, const타입의 string 파라미터를 받는 함수 포인터 정의

Type aliasestd::function 을 함께 사용하기.

using comparisonFcn = std::function<bool(int, int)>; 
void selectionSort(int* array, int size, comparisonFcn compare)

 // 이 부분도 comparisonFcn 이 아닌 compare로 바꿔 주어야 한다
 // 세번째 인자로 받은 함수 펑션을 호출한다. comparisonFcn은 두개의 int 값을 비교하여 True or False를 리턴한다.
 if (compare(array[bestIndex], array[currentIndex]))
 {
 	bestIndex = currentIndex;
 }

 

이 포스트의 원문은 www.learncpp.com/cpp-tutorial/78-function-pointers/ 

 

7.8 — Function Pointers | Learn C++

7.8 — Function Pointers By Alex on August 8th, 2007 | last modified by nascardriver on August 8th, 2020 In lesson 6.7 -- Introduction to pointers, you learned that a pointer is a variable that holds the address of another variable. Function pointers are

www.learncpp.com

 

+ Recent posts