Post

C++ - 함수포인터, 함수객체, 람다

C++

C++ - 함수포인터, 함수객체, 람다

함수 포인터

함수 포인터는 사용자가 정의한 함수의 메모리 주소를 저장하고, 해당 함수를 간접적으로 호출할 수 있게 하는 기능이다.

생성과 호출

1
2
3
4
	void(*funcPtr)() = Func1;
	(*funcPtr)();
	funcPtr = Func2;
	funcPtr();

함수 포인터는 반환타입(*포인터명)(매개변수 타입) = 포인터를 받아올 함수; 이렇게 정의할 수 있다.

호출은 (*포인터)(); 이게 정석이지만, C++에선 함수를 암묵적으로 함수 주소로 변환해주기에 포인터(); 처럼 호출도 가능하다.

특징

함수 포인터의 특징들을 알아보자.

함수 포인터 매개변수

함수 포인터는 다른 함수의 매개변수로 사용이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int Quick(vector<int>& v, int left, int right, bool(*compare)(int num1, int num2))
{
	int pivot = v[right];
	int leftIndex = left;

	for (int i = left; i < right; i++)
	{
		if (compare(v[i], pivot))
		{
			swap(v[i], v[leftIndex++]);
		}
	}

	swap(v[leftIndex], v[right]);

	return leftIndex;
}

using compareFunc = bool(*)(int, int);

void QuickSort(vector<int>& v, int left, int right, compareFunc compare) //using으로 보기 좋게 변경 가능
{
	if (left < right)
	{
		int pivot = Quick(v, left, right, compare);

		QuickSort(v, left, pivot - 1, compare);
		QuickSort(v, pivot + 1, right, compare);
	}
}

bool Ascending(int num1, int num2)
{
	return num1 < num2;
}

bool Descending(int num1, int num2)
{
	return num2 < num1;
}

int main()
{
  vector<int> v = { 1,5,2,8,9,43,7,3 };

  QuickSort(v, 0, v.size() - 1, Ascending); //함수 포인터를 매개 변수 넣음

  for (int i = 0; i < v.size(); i++)
  {
	  cout << v[i] << " "; 
  }
  cout << endl;
  QuickSort(v, 0, v.size() - 1, Descending);
}

클래스 멤버함수

클래스 멤버함수의 경우 일반적인 함수 포인터와는 생성 방식과 호출 방식이 다르다.

1
2
3
4
5
6
7
8
TestClass test1("테스트1"), test2("테스트2");
void (TestClass::*memberFuncPtr)() = &TestClass::MemberFunc;

(test1.*memberFuncPtr)();
(test2.*memberFuncPtr)();

void (*staticMemberFuncPtr)() = &TestClass::StaticMemberFunc;
staticMemberFuncPtr();

클래스 멤버함수를 생성하기 위해선 범위 지정 연산자를 이용해 해당 클래스의 멤버 함수임을 명시해야 한다.

또한 멤버함수 포인터의 경우 단독으로 호출하지 못하고, 객체에 의존해야한다.

반대로 정적 멤버함수의 경우 객체와 독립적이기에 일반 함수 포인터처럼 생성, 호출이 가능하다.

템플릿 함수

템플릿 함수도 함수 포인터로 간접 호출이 가능하다.

1
2
3
4
5
6
7
8
9
10
  template<typename T>
  void Print(T value)
  {
	  cout << value << endl;
  }

	void(*templateFuncPtrInt)(int) = Print<int>; // int 타입에 대한 템플릿 함수 포인터
	void(*templateFuncPtrStr)(string) = Print<string>; // string 타입에 대한 템플릿 함수 포인터
	templateFuncPtrInt(10);
	templateFuncPtrStr("스트링");

각 타입마다 포인터를 따로 생성해야 한다.

함수 포인터를 사용할 땐, 템플릿 파라미터 자동 유추가 불가능하여 구체적인 타입을 명시해야한다.

함수 포인터 배열

함수 포인터도 배열에 넣어 관리가 가능하다.

1
2
3
4
	void(*funcArr[])() = { Func1, Func2 };

	funcArr[0]();
	funcArr[1]();

함수 포인터를 사용하면, 프로그램 실행 중에 함수 호출을 동적으로 결정하기에 수월하다.

사실 함수 포인터와 비슷하게 동작하면서 더 좋은 람다를 더 많이 사용하다보니 자료가 별로 없는 것 같다.

물론 콜벡 함수나, 전략 패턴에서 사용이 가능하다.

함수 객체

함수 객체는 사용자 지정 구조체를 생성하여 () 오퍼레이터를 재정의 하는 방식으로 함수를 호출한다.

클래스를 통해 만들기 때문에, 동작을 지원하는 추가적인 함수나 변수를 추가할 수 있다.

생성과 호출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct TestFunctor
{
	void operator()()
	{
		cout << "Functor 호출" << endl;
	}

	void operator()(int num)
	{
		cout << "매개변수: " << num << endl;
	}
};

int main()
{
  TestFunctor functor;
  functor();
}

구현은 정말 간단하다. 위에서 언급했듯, 단지 () operator를 정의해주면 된다.

또한 오버로딩 또한 가능하기에 한 곳에 모아둘 수 있어 좋다.

특징

상태 저장

클래스로 만들어지기에, 멤버 변수를 활용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Counter
{
public:
	void operator()()
	{
		cout << "Count: " << ++counter << endl;
	}

private:
	int counter = 0;
};

int main()
{
	Counter countFunctor;
	countFunctor();
	countFunctor();
	countFunctor();
	countFunctor();
	countFunctor();
}

위처럼 멤버 변수를 이용해, 함수 객체의 상태를 저장 및 갱신할 수 있다는 장점이 있다.

STL 호환성성

또한 STL에서 Functor를 자주 사용하기에 STL 함수와 호환성이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Compare
{
	bool operator() (pair<int, string> p1, pair<int, string> p2)
	{
		return p1.first < p2.first;
	}
};

vector<pair<int, string>> v;
v.push_back({2, "string2"});
v.push_back({1, "string1"});

sort(v.begin(), v.end(), Compare());

for (int i = 0; i < v.size(); i++)
{
	cout << v[i].first << " " << v[i].second << endl;
}

위 코드는 사용자 지정 자료형을 정렬할 때, 위처럼 함수 객체를 매개변수로 넣어 비교 기준을 정해주는 코드다.

템플릿

함수 객체도도 물론 템플릿 사용이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TemplateFunctor
{
public:
	template<typename T>
	void operator()(T value)
	{
		cout << "매개변수의 타입: " << typeid(value).name() << " ";
		cout << "매개변수 값: " << value << endl;
	}
};

TemplateFunctor templateFunctor;
templateFunctor(5);
templateFunctor(5.0);

함수 객체의 경우, 상태와 추가적인 함수를 지원할 수 있다.

덕분에 함수 포인터보다 더 복잡한 동작에 적합하다.

물론 객체 생성이 필수적이기에 객체 생성 오버헤드가 있다는 단점이 있다.

람다

람다는 C++11에서 추가된 기능이다.

람다또한 함수를 호출하기 위해 사용하는 객체로, 좀 더 향상된 함수 객체라고 생각해도 될 것 같다.

생성과 호출

1
[]()->void {cout << "클로저 객체(임시 객체) 생성 및 호출" << endl;}();

[]()->void {cout << "클로저 객체(임시 객체) 생성 및 호출" << endl;} 여기까지가 생성이고, 뒤에 ()가 호출이다.

람다는 이렇게 함수를 선언할 필요 없이, 그 자리에서 함수 기능을 구현할 수 있어 간결하다는 특징이 있다.

특징

람다의 구성

위에서 생성한 람다를 보면,

[]()-> void {}

4가지로 구성되는 것을 확인할 수 있다.

  • 개시자 : [] 이 기호를 개시자라고 부르며 외부 변수를 넣으면 람다 함수가 캡쳐하여, 람다 내부에서 이용 가능하다.
  • 인자 : ‘()’ 기호이며, 함수의 ‘()’ 기호와 동일하다. 매개변수를 넣을 수 있다. 인자가 아무것도 없으면 생략 가능하다.
  • 반환 타입 : -> void 위 예시처럼 반환 타입을 선언한다. 반환 타입이 void-> void 자체를 생략 가능하다.
  • 함수 몸통 : {} 함수 기능을 구현하는 부분이다.

캡쳐

람다의 개시자에 외부 변수를 넣으면 람다 내부에서도 사용 가능하다.

  • [&] : 외부의 모든 변수들을 참조로 가져온다.
  • [=] : 외부의 모든 변수들을 값으로 가져온다.
  • [=, &x, &y] : x, y는 참조로, 나머지는 값으로 가져온다.
1
2
int num = 0;
[&num]()->void { cout << "외부 데이터 접근가능: " << num << endl; }(); //참조로 가져옴

캡쳐를 값으로 할 때, 자동으로 const가 붙어 값을 바꿀 수 없다.

[num]() {num = 1;};

위처럼 사용하면 const라 오류가 발생한다.

만약 람다 내부에서 num을 변경하려면 mutable 키워드를 사용해야한다.

[num]() mutable {num = 1;};

하지만 클래스 내에선 재밌는 일을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LambdaCounter
{
public:
	LambdaCounter() {}

	void AddOne()
	{
		[=]() {count++;}();
	}

	int count = 0;
};

LambdaCounter counter;
cout << counter.count << endl;
counter.AddOne();
cout << counter.count << endl;

위 코드는 LambdaCounter 클래스의 AddOne 함수를 호출해 멤버변수 count를 증가시킬 수 있다.

클래스 내에서 값을 캡처하면 암묵적으로 this를 캡쳐할 수 있는다.

당연히 thisconst이기에 변경하지 못하지만, this의 멤버 변수인 countconst가 아니기에 변경이 가능하다.

템플릿

클로져 객체도 타입이 있으니, 템플릿에서 사용이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
void TemplateFill(vector<int>& v, T func)
{
	int num = 0;

	while (func())
	{
		v.push_back(num++);
	}
}

int total = 0;

vector<int> v;

TemplateFill(v, [&v]()->bool {return v.size() < 10;});

for_each(v.begin(), v.end(), [&total](int num) {total += num;}); //STL 에서도 사용 가능함.

cout << "벡터 안 요소들의 합: " << total << endl;

이 코드는 람다 함수로 조건을 검사하며 매개변수로 받은 벡터에 값을 추가하는 코드다.

물론 STL에서도 사용이 가능하다.

저장

클로저 객체를 저장하는 방법은 3가지가 있다.

  1. auto 사용하기
    • 람다 함수는 컴파일 타임에 생성되는 고유 클래스로 처리되기에 명시적인 타입을 알기 힘들다.
    • 대신 auto 키워드를 사용하면 클로져 객체 저장할 수 있다.
  2. 함수 포인터 사용하기
    • 함수 포인터를 사용하면 람다 함수를 저장할 수 있다.
    • 대신 아무것도 캡쳐하지 않을 때만 가능한 방법이다.
  3. std::function 사용하기
    • std::function을 이용하면 함수 포인터, 함수 객체, 람다를 저장할 수 있다.
1
2
3
4
auto autoLambda = [] {cout << "auto로 저장한 람다" << endl;};
void(*funcPtrLamda)() = [] {cout << "함수 포인터로 저장한 람다" << endl;};
autoLambda();
funcPtrLamda();

위 코드에선 auto와 함수 포인터를 사용해 람다를 저장했다.

변수로 저장한 람다는 원하는 타이밍에 호출할 수 있다.

std::function의 경우, 나머지 2개를 다루는 법을 정리하기 위해 추가하지 않았다.

캡처 기준

값으로 캡쳐하는 상황에선 클로져 객체가 생성될 때의 기준 값이 저장되니 주의해야한다.

1
2
3
4
	int tmp = 0;
	auto captureTime = [tmp] {cout << "tmp: " << tmp << endl;};
	tmp = 10;
	captureTime();

참조 캡쳐는 문제 없다.

뭔가 만들어보고 싶은데, 좋은 아이디어가 떠오르지 않는다…

그래도 알고리즘 문제 풀 때는 많이 사용하니까~

This post is licensed under CC BY 4.0 by the author.