뽀또치즈맛 2025. 4. 12. 22:32

 

람다 함수

 

람다도 초보자에겐 생소한데

람다 함수"들"? 이라고 하니 좀 더욱 더 생소할 것이다.

 

지금부터 람다에 대해서 간략하게 설명하겠다.

 

람다(lambda) 함수(람다 표현식 또는 간단히 람다라고도 함)를 보면

초보 프로그래머에게 도움이 될 수 있도록 추가된

C++11의 기능이 아니라고 의심할 수 있다.

람다 함수는 겉보기에도 그런 의구심을 가질 수 있기 때문이다.

람다 함수의 예제이다.

 

[&count] (int x} {count += (x % 13 == 0);}

 

 

하지만 보이는 것 만큼 미스터리한 것은 아니며

특히 함수를 사용하는 STL 알고리즘에서 유용한 서비스를 제공한다.

 

 

함수 포인터, 펑크터 그리고 람다 사용법

 

STL 알고리즘에 정보를 반영하는 세 가지 접근법을 사용한 예제를 살펴보자.

우선 이 세가지는 함수 포인터, 함수 객체 펑크터(functor) 그리고 람다이다.

 

편의를 위해 함수 객체로써 세 가지 형태를 참고하여

"함수 포인터나 함수 객체 또는 람다"를 반복하지 않도록 한다.

 

이 세가지를 표현하기 쉽게 코드를 짜는 상황을 다음과 같이 가정해보자.

랜덤 정수 리스트를 생성하고 얼마나 많은 것이 3으로 나뉘는지,

13으로 나뉘는 것은 몇개인지 확인하는 경우이다.

필요에 따라 굉장히 흥미로운 일을 찾게 될 것이다.

 

리스트를 생성하는 것은 꽤 쉽다.

한 가지 선택 사항은 숫자를 저장하기 위해

vector<int> 배열을 이용하고 랜덤 숫자를 가진 배열을 채우기 위해

STL generate() 알고리즘을 이용하는 것이다.

 

#include <vector>
#include <algorithm>
#include <cmath>

...


std::vector<int> numbers(1000);
std::generate(vector.begin(), vector.end(), std::rand);

 

 

generate() 함수에 첫 두 개 매개변수로 범위를 지정하고

세 번째 매개변수에 반환될 값을 각각 설정하는데,

함수 객체는 매개변수를 갖지 않는다.

이번 예제에서 함수 객체는 표준 rand() 함수의 포인터이다.

 

count_if() 알고리즘의 도움으로 3으로 나뉘는 매개변수의 개수를 세는 것은 쉽다.

처음 두 매개변수는 반드시 범위안에 있어야 하며, generate()만을 위한 것이다.

세 번째 매개변수는 함수 객체이어야 하며, true 또는 false를 반환한다.

 

 

++PLUS++

여기서 잠깐 함수 개체를 설명하겠다.

 

함수 객체라 함은,

즉 c++ 표준 라이브러리에서 일컫는 함수 개체(= 객체)라 함은

 

함수 개체 또는 함수는 operator()를 구현한 형식을 일컫는다.

이 연산자는 호출 연산자 또는 경우에 따라 애플리케이션 연산자라고도 한다.

C++ 표준 라이브러리는 기본적으로

함수 개체를 컨테이너의 정렬 기준 및 알고리즘에서 사용한다.

 

함수 개체는 직접적인 함수 호출에 비해 두 가지 주요 장점을 제공한다.

 

첫 번째는 함수 개체에 상태를 포함할 수 있다는 점이다.

두 번째는 함수 개체가 형식이므로 템플릿 매개 변수로 사용할 수 있다는 점이다.

 

함수 개체 만드는 코드

class Functor
{
public:
    int operator()(int a, int b)
    {
        return a < b;
    }
};

int main()
{
    Functor f;
    int a = 5;
    int b = 7;
    int ans = f(a, b);
}

 

 

main 함수의 마지막 줄은 함수 개체를 호출하는 방법을 보옂ㄴ다.

이 호출은 함수에 대한 호출처럼 보이지만 실제로 Functor 형식의 연산자()를 호출한다.

함수 개체 호출과 함수의 이러한 유사성은 함수 개체라는 용어가 나타난 방식이다.

 

함수 개체 및 컨테이너 : 함수 개체의 용도 1 <컨테이너 정렬 기준>

 

C++ 표준 라이브러리는 헤더 파일에 여러 함수 개체를

<functional> 에 포함한다.

이러한 함수 개체의 용도 중 하나는 컨테이너의 정렬 기준이다.

예를 들어 st 컨테이너는 다음과 같이 선인된다.

template <class Key,
    class Traits=less<Key>,
    class Allocator=allocator<Key>>
class set

 

두 번째 템플릿 인수는 함수 개체 less이다.

이 함수 개체는

첫 번째 매개 변수가 두 번째 매개 변수보다 작은 경우 반환을 true로 한다.

일부 컨테이너는 해당 요소를 정렬하므로 컨테이너에는 두 요소를 비교하는 방법이 필요하다.

비교는 함수 개체를 사용하여 수행된다.

함수 개체를 만들고 컨테이너에 대한 템플릿 목록에서 지정하여

컨테이너의 고유한 정렬 기준을 정의할 수 있다.

 

함수 개체 및 알고리즘 : 함 개체의 용도 2 <알고리즘>

 

함수 개체의 또 다른 용도는 알고리즘이다.

예를 들어 remove_if 알고리즘은 다음과 같이 선언된다.

 

remove_if에 대한 마지막 인수는 부울 값을 반환하는 개체 함수 (predicate)이다.

함수 개체의 결과가 true이면 요소가 first 및 last 반복기에서 액세스하는 컨테이너에서 제거된다.

인수 pred에 대해 헤더에 <functional> 에 선언된 함수 개체를 사용하거나 직접 만들 수 있다.

 

 

++++

 

 

count_if() 함수는 true로 반환하는 함수 객체에 대한 모든 매개변수를 세기 시작한다.

3으로 나뉠 수 있는 값을 찾으면 다음과 같은 함수 정의를 사용하면 된다.

 

bool f3(int x) { return x % 3 == 0; }

 

 

유사하게 13으로 나뉘는 매개변수를 찾기 위해 다음과 같은 함수를 사용할 수 있다.

bool f13(int x) { return x % 13 == 0; }

 

 

이런 정의된 함수들로 다음과 같이 매개변수를 센다.

 

int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
cout << "3으로 나누어지는 매개변수 수 : " << count3 << '\n'

int count13 = std::count_if(numbers.begin(), bnumbers.end(), f13);
cout << "13으로 나누어지는 매개변수 수 : " << count13 << "\n\n";

 

 

다음으로 펑크터를 이용하여 같은 작업을 어떻게 수행하는지 검토해 본다.

 

++Plus++

 

펑터 또는 펑크터라고 한다.

펑크터는 많은 STL 알고리즘들이 퐁크터(functor)라고 부르는

함수 객체(Function object)를 많이 사용한다.

펑크터는 함수처럼 ()과 함꼐 사용할 수 있는 객체이다.

일반 함수 이름,

함수를 지시하는 포인터,

() 연산자가 오버로딩된 클래스 객체 모두 펑터가 될 수 있다.

#include <iostream>
class Money {
private:
       int _Money = 0;
public:
       int operator()() {
              return this->_Money;
       }
       void operator()(int N) {
              this->_Money += N;
       }
};
int main(void) {
       Money money;
       money(100); //void operator()(int)
       int M = money(); //int operator()()
       std::cout << M;
}

 

 

오버로딩된 ()연산자가 Money 객체들을 함수처럼 사용하는 것을 허용한다.

 

++++

 

펑크터는 클래스 함수와 같이 클래스가 operator()()를 정으히가 때문에

함수 이름이 있었던 것처럼 사용하기보다는 클래스 객체로 사용하는 것이다.

펑크터의 한 가지 장점은 예제에서 수를 세는 작업을 하는 데 같은 펑크터를 사용할 수 있다는 것이다.

다음은 한가지 예이다.

 

class f_mod
{
private:
	int dv;

public:
	f_mod() { dv = 0; }
	f_mod(int d = 1) : dv(d) {}
	bool operator() (int x) { return x % dv == 0; }

};

 

 

이 과정이 이렇게 수행되는지 다시 생각해 보자.

특정 정수값을 저장하는 f_mod 객체를 만들기 위해 생성자를 이용할 수 있다.

 

f_mod obj(3); // f_mode.dv는 3으로 설정한다.

 

 

이 객체는 bool 값을 반환하는 operator() 함수를 이용한다.

bool is_div_by_3 = obj(7); // obj.operator()(7)과 같다.

 

 

생성자 자신은 count_if()와 같이 함수에 매개변수로 사용될 수 있다.

 

count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));

 

 

f_mod(3)은 값 3을 저장하는 객체를 생성하고,

count_if()는 생성된 객체를 사용하여 perator()() 함수를 호출한다.

매개변수 x는 매개변수의 개수와 같은 값으로 설정된다.

얼마나 많은 숫자가 3대신 13으로 나뉠 수 있는지

세기 위해서 f_mod(13)을 세 번째 매개변수로 사용한다.

 

마지막으로 람다 형식으로 접근해 보도록 하자.

람다라는 이름은 함수를 정의하고 적용하기 위해

람다 미적분학, 수학적 시스템으로부터 온 것이다.

시스템은 익명 함수를 사용하는 것, 즉 함수 이름을 생략하는 것이 가능하다.

 

C+11 구문에서는 익명 함수 정의(람다)를

함수 포인터나 함수 기호 대신 함수의 매개변수로 사용할 수 있다.

람다는 f3() 함수에서 다음과 같이 적용한다.

[](int x) {return x % 3 == 0; }

 

 

다음 코드가 보다 f3()의 정의인 것처럼 보인다.

bool f3(int x) { return x % 3 == 0; }

 

두 차이점은 함수 이름이 [](얼마나 알 수 없는 형태인가..)로 변형 되었고

반환 타입이 선언되지 않았다는 것이다.

대신 반환 타입은 decltype이 반환 값으로부터

추정된 타입이 되고, 이 경우에 bool형이 된다.

람다가 반환 구문을 가지고 있지 않다면,

타입은 void형이 된다.

 

다음 예제는 이 람다를 사용한 것이다.

 

count3 = std::count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0; });

 

즉, 전체 람다 표현식으 ㄹ포인터나 펑크터 생성자로 대신 사용할 수 있다.

 

람다에서 본문이 단일 구문을 반환하는 구조라면 타입은 자동으로 결정된다.

그렇지 않다면 다음과 같이 새로운 반환 값을 추정하는 문법이 필요하다.

 

[](double x)->double {int y = x; return x - y; } // 반환 타입은 double형이다.

 

 

다음은 람다 식을 이용한 코드 전문이다.

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>

const long Size1 = 39L;
const long Size2 = 100 * Size1;
const long Size3 = 100 * Size2;
bool f3(int x) { return x % 3 == 0; }
bool f13(int x) { return x % 13 == 0; }


int main()
{
	using std::cout;
	std::vector<int> numbers(Size1);
	std::srand(std::time(0));
	std::generate(numbers.begin(), numbers.end(), std::rand);

	// 함수 포인터 사용
	cout << "샘플 크기 =  " << Size1 << '\n';

	int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
	cout << "3으로 나누어질 수 있는 개수  : " << count3 << '\n';

	int count13 = std::count_if(numbers.begin(), numbers.end(), f13);
	cout << "13으로 나누어질 수 있는 개수  : " << count13 << '\n';

	// 숫자 증가시킴
	numbers.resize(Size2);
	std::generate(numbers.begin(), numbers.end(), std::rand);
	cout << "Sample size = " << Size2 << '\n';
	
	// 함수 객체 (= 펑크터) 사용
	class f_mod
	{
	public:
		f_mod() { dv = -1; }
		f_mod(int d = 1) : dv(d) {}
		bool operator()(int x) { return x % dv == 0; }

	private:
		int dv;
	};

	count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));
	cout << "3으로 나누어질 수 있는 개수 : " << count3 << '\n';

	count13 = std::count_if(numbers.begin(), numbers.end(), f_mod(13));
	cout << "13으로 나누어질 수 있는 개수 : " << count13 << "\n\n";

	// 숫자 다시 증가 시킴
	numbers.resize(Size3);
	std::generate(numbers.begin(), numbers.end(), std::rand);
	cout << "Sample size = " << Size3 << '\n';

	// 람다 사용
	count3 = std::count_if(numbers.begin(), numbers.end(), 
		[](int x) {return x % 3 == 0; });
	cout << "3으로 나누어질 수 있는 개수 : " << count3 << '\n';

	count13 = std::count_if(numbers.begin(), numbers.end(),
		[](int x) {return x % 13 == 0; });
	cout << "13으로 나누어질 수 있는 개수 : " << count13 << "\n\n";



	return 0;
}

 

 

코드 실행 결과

 

"왜" 람다인가?

 

그럼 왜 람다를 사용할까?

왜 람다들을 사용할까?

의구심이들 것이다.

아니 잘만 함수 만들어놓고

왜 함수 객체, 람다식을 사용하는 것,

함수 포인터 사용을 언급하며 자꾸 설명할까 싶을 것이다.

 

그 왜가 중요한 것이다.

식을 외우는 건 사실 왜를 알고 보면 외워진다.

사실 그냥 읽히는 것이다.

 

람다는 기본적으로 특별한 표현식이다.

이 문제를 크게 네 가지 관점에서 살펴보자

 

  1. 근접성
  2. 간결함
  3. 효율
  4. 능력

 

 

1. 근접성 : 근접성이 높으면 가독성이 높다 둘은 비례 관계성을 가진다.

 

많은 프로그래머들은 그들이 사용하고 싶은 곳 가까이에

필요한 것을 정의하는 것이 유용하다고 느낀다.

다시 말해 count_if() 함수에서

첫 번째 매개변수가 호출하는 것이 무엇인지를 소스 코드에서 찾기 위해

페이지를 스캔하고 싶지 않다는 것이다.

 

또한 코드를 수정하고자 할 때 모든 컴포넌트가 손에 닿기 쉬운 곳에 있길 원한다.

그리고 다른 곳에서 사용할 코드를 자르고 붙일 때 

다시 모든 컴포넌트가 가까이 있어야 한다.

 

이런 의미에서 람다는 정의하는 것이

사용 관점에서 좋기 때문에 이상적이라는 것이다.

 

(함수의 가독성 측면 단점 -> 접근성이 낮다.)

함수는 다른 함수에 정의될 수 없다는 이유로 이런 의미에서 함수는 최악이며

정의는 사용하고자 하는 시점으로부터 멀리 위치해 있다.

 

(펑크터의 가독성 측면 장점 -> 접근성이 좋다.)

그러나 펑크터는 펑크터 클래스를 포함하는 클래스가 함수 내에 정의될 수 있기 때문에

꽤 괜찮고, 정의는 사용 관점으로부터 가까이 위치한다.

 

사용자의 피로함과 번거로움이 낮아지는 것이다.

 

2. 간결함 : 이름 없는 함수라고 정의를 두 번 작성하지 않아도 된다.

 

펑크터의 코드는 간결함에 대해선 함수나 람다 코드보다 설명할 것이 많다.

함수와 람다는 거의 똑같이 짧지만,

 

한 가지 분명히 다른 점은 람다를 두 번 사용해야 하는 경우이다.

	count3 = std::count_if(numbers.begin(), numbers.end(),
		[](int x) {return x % 3 == 0; });

	count13 = std::count_if(numbers.begin(), numbers.end(),
		[](int x) {return x % 13 == 0; });

 

 

하지만 사실상 람다를 두 번 작성하지 않아도 된다.

근본적으로 이름 없는 람다를 위해 이름을 생성하여 

그 이름을 두 번 사용하면 된다.

	auto mod3 = [](int x) {return x % 3 == 0; }; // mod3는 람다를 위한 이름
	int count1 = std::count_if(numbers1.begin(), numbers1.end(), mod3);
	int count2 = std::count_if(numbers2.begin(), numbers2.end(), mod3);

 

일반적인 함수처럼 더 이상 익명이 아닌 람다를 사용할 수 있다.

	bool result = mod3(z); // z % 3 == 0이면 true를 반환한다.

 

3. 효율성 : 컴파일러의 람다 추적

그러나 일반 함수와 달리 이름이 있는 람다는 함수 내에 정의될 수 있다.

mod3의 실제 타입은 컴파일러가 람다를 추적하여 구현되는 것에 따른 타입으로 결정된다.

 

이 세 가지 접근법의 상대적 효과는

결국 컴파일러가 인라인화가 가능한지에 달려 있다.

함수 주소의 개념이 인라인이 아닌 함수를 의미하기 때문에,

 

컴파일러가 전통적으로 주소를 가지고 있는 함수를

인라인하지 않는 사실이 함수 포인터 접근을 불편하게 한다.

펑크터와 람다로는 인라인 떄문에 발생하는 눈에 띄는 모순은 없다.

 

4. 능력 : 람다의 몇 가지 추가 기능 : 참조와 자동화, 혼합 사용

 

마지막으로 람다는 몇 가지 추가 기능을 가지고 있다.

특히 람다는 범위 내에서 모든 자동화된 변수 읾으로 접근이 가능하다.

사용될 변수는 괄호 사이에 이름을 넣어 접근할 수 있다.

[z]와 같이 단지 이름만 사용한다면 변수는 값으로 접근된다.

[&count] 처럼 이름 앞에 &를 두어 참조로 변수를 접근할 수도 있다.

그리고 [&]를 잉ㅇ하여 참조한 모든 자동화된 변수를 접근할 수 있고

[=]는 값으로 모든 자동화된 변수를 접근한다.

 

혼합하여 사용하는 것도 가능하다.

예를 들어 [ted, &ed]는 값에 의한

ted와 참조에 의한 ed를 접근하는 것이고,

[&, ted]는 값에 의한 ted와 참조에 의한 모든 다른 자동화된 변수에 접근하며,

[=, &ed]는 ed에 참조에 의한, 자동화된 변수로 남아 있는 것에 값에 의한 접근이 가능하다.

 

그래서 아래와 같은 코드를 

    int count13;
    count13 = std::count_if(numbers.begin(), numbers.end(),
	    [](int x) {return x % 13 == 0; });

 

다음과 같이 변경할 수 있다.

	int count13;
	count13 = std::count_if(numbers.begin(), numbers.end(),
		[&count13](int x) {return count13 += x % 13 == 0; });

 

 

[&count13]은 코드에서 람다는 count13을 사용하는 것을 허용한다.

count13은 참조에 의해 접근하며,

람다에서 count13의 모든 변화는 원래 count13에 영향을 미친다.

x가 13으로 나뉠 수 있는 경우 표현식 x % 13 == 0dms true를 반환하고,

ture는 1로 변환되어 count13에 더해진다.

같은 맥락으로 flase는 0으로 변환된다.

그러므로 for_each()에서 각 숫자가 람다 표현식에 적용되고

count13은 13으로 나뉠 수 있는 매개변수의 개수를 센다.

 

이 방법을 이용하여 간단한 람다 표현식으로

3을 나뉘는 수와 13으로 나누는 매개변수 수를 셀 수 있다.

 

	int count3 = 0;
	int count13 = 0;
	std::for_each(numbers.begin(), numbers.end(),
		[&](int x) { count3 += x % 3 == 0; count13 += x % 13 == 0; });

 

이번에는 count3과 count13을 포함한 [&]가

모든 변수를 자동으로 생성하는 람다 표현식을 사용하는 것을 알아본다.

 

 

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>

const long Size = 390000L;


int main()
{
	using std::cout;
	std::vector<int> numbers(Size);

	std::srand(std::time(0));
	std::generate(numbers.begin(), numbers.end(), std::rand);
	cout << "샘플 크기 =  " << Size << '\n';
	// 람다 사용
	int count3 = std::count_if(numbers.begin(), numbers.end(),
		[](int x) {return x % 3 == 0; });
	cout << "3으로 나누어질 수 있는 개수  : " << count3 << '\n';

	int count13 = 0;
	std::for_each(numbers.begin(), numbers.end(),
		[&count13](int x) {count13 += x % 13 == 0; });
	cout << "13으로 나누어질 수 있는 개수  : " << count13 << '\n\n';
	// 간단한 람다 사용
	count3 = count13 = 0;
	// 람다 사용
	std::for_each(numbers.begin(), numbers.end(),
		[&](int x) { count3 += x % 3 == 0; count13 += x % 13 == 0; });

	cout << "3으로 나누어질 수 있는 개수  : " << count3 << '\n';
	cout << "13으로 나누어질 수 있는 개수  : " << count13 << '\n\n';

	return 0;
}

 

++++

 

Plue  정리 참고 문서

https://learn.microsoft.com/ko-kr/cpp/standard-library/function-objects-in-the-stl?view=msvc-170

 

C++ 표준 라이브러리의 함수 개체

자세한 정보: C++ 표준 라이브러리의 함수 개체

learn.microsoft.com

https://learn.microsoft.com/ko-kr/cpp/standard-library/algorithms?view=msvc-170

 

알고리즘

자세한 정보: 알고리즘

learn.microsoft.com