포스트

[함수 포인터] 04. 함수 포인터를 보기 좋게 만들기

이전에 함수 포인터는 매우 유연한 프로그래밍을 만들기 좋은 도구지만, 매번 함수 포인터를 만들기에는 조금 코드가 많아지거나 보기 불편할 수 있습니다.

이를 간단하게, 보기 좋게 만드는 방법은 몇 가지 존재합니다.

  • typedef로 기존의 타입에 새로운 별칭을 만들어주는 방법. (새로운 타입처럼 사용하기)
  • using을 사용하여 기본 타입에 새로운 별칭을 만들어 사용하는 방법. (C++11 이후)
  • <functional> 라이브러리를 사용하는 방법입니다.

typedef로 별칭 만들어 사용하기

함수 포인터도 하나의 타입으로써 typedef를 사용해서 타입 별칭을 만들어 사용해줄 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void func1(int a, int b)
{
	std::cout << "func1: " << (a + b) << '\n';
}

int main()
{
	// typedef
	typedef void(*FuncType)(int, int);

	FuncType func = func1;
	func(1, 2);
}

위 처럼 typedef를 사용하면, 인수를 (int, int)로 가지는 함수 포인터 타입FuncPtr로 만들어 사용할 수 있게 됩니다.

함수 포인터 보기 좋게 만들기 typedef

using으로 별칭 만들어 사용하기

C++ 11 이후부터 사용할 수 있는 usingtypedef와 비슷하게 사용할 수 있습니다.

다만, using은 마치 변수를 선언하듯 using 별칭에 타입을 대입해 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void func2(double a, double b)
{
	std::cout << "func2: " << (a * b) << '\n';
}

int main()
{
	// using
	using FuncUsing = void(*)(double, double);

	FuncUsing func = func2;
	func(1, 2);
}

위처럼 사용하면 인수를 (double, double)로 가지는 타입을 FuncUsing이라는 별칭으로 사용할 수 있습니다.

함수 포인터 보기 좋게 만들기 using

typedef와 using의 차이점

최근 C++을 배우시는 분들은 typedef는 자주 못봤어도 using은 자주 보셨을거라 생각됩니다.

왜냐하면, 기본적으로 네임스페이스를 파일 내에서 간편하게 사용하기 위해 다음과 같은 문법을 사용하기 때문입니다.

1
using namespace std;

typedef는 최근에 배우는지는 모르겠지만, 예전에 C를 배울 때 구조체 선언에서 자주 쓰였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct MyStruct
{
	int a;
} MS;

int main()
{
	// typedef를 사용하지 않은 경우 struct 키워드를 사용해야 한다.
	struct MyStruct myStruct;
	myStruct.a = 10;

	// typedef를 사용한 경우 struct 키워드를 사용하지 않아도 된다.
	MS myStruct2;
	myStruct2.a = 10;
}

즉, typedef는 C에서 사용하던 키워드고 C++에서는 using으로 자주 쓰입니다.

typedef의 문법 구조

typedef는 기존 struct를 선언하던 부분을 잘 보시면, 다음과 같이 되어있는 것을 볼 수 있습니다.

typedef 기존_타입 별칭명;

즉, 사용하다 보면 별칭명이 눈에 잘 띄지 않는데다가 다소 복잡한 타입에 대해서는 별칭을 만들기 어렵습니다.

1
2
3
typedef int Integer;
typedef std::vector<int> IntVector;
typedef void (*FuncPtr)(int, double);

위의 두 타입은 기존 문법 구조를 잘 따라가는데, 함수 포인터명은 중간에 들어가야 합니다.

using의 문법 구조

typedef에 비해 using은 매우 자주 보던 형태의 문법 구조를 띄고 있습니다.

using 별칭명 = 기존_타입;

즉, 다소 복잡한 타입이여도 별칭명이 왼쪽에 정렬되어 있어 보기도 편하고 만들기도 쉽습니다.

1
2
3
using Integer = int;
using IntVector = std::vector<int>;
using FuncPtr = void (*)(int, double);

보시면 모든 별칭은 using과 함께 있고, 그에 타입을 대입하는 형태로 되어있어 만들기도 편합니다.

using을 자주 사용하는 이유

using을 자주 사용하는 이유는 위에서 말한바와 같이 두가지 이유(일관된 문법, 좋은 가독성)도 있지만, 한 가지 더 있습니다.

그건 바로 C++에서 추가된 제네릭 타입에 대한 지원을 using이 해주기 때문입니다.

1
2
3
4
5
6
7
typedef template<typename T1>	// typedef는 템플릿 타입을 받을 수 없다.
std::vector<T1> IntVector;

template<typename T1>
typedef std::vector<T1> IntVector;	// template 뒤에 typedef를 붙일 수 없다.

typedef std::vector<int> IntVector; // 이건 가능

위의 두 예시를 보시면 아시겠지만, typedef는 템플릿 인스턴스에 대한 별칭을 만드는 데 사용할 수 있지만, 템플릿 자체에 대한 별칭을 만드는 것은 할 수 없습니다.

하지만, using은 제네릭 타입에 사용 가능합니다.

1
2
template<typename T>
using IntVector = std::vector<T>; // 가능

그 외에도

  • namespace 가져오기(using namespace std)
  • namespace의 멤버 별칭 가져오기(using std::cout)

등으로 많이 사용됩니다.

<functinal> 사용하기

typedefusing을 사용하는 것 외에도 <functional> 헤더에 정의되어 있는 std::function을 이용해서 함수 포인터를 정의할 수도 있습니다.

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
#include <iostream>
#include <functional>

class MyClass
{
public:
	void Add(int a, int b)
	{
		std::cout << "MyClass Add: " << (a + b) << std::endl;
	}
};

void Add(int a, int b)
{
	std::cout << "Add: " << (a + b) << std::endl;
}

int main()
{
	std::function<void(int, int)> FuncPtr;

	FuncPtr = Add;
	FuncPtr(1, 2);

	MyClass myClass;
	FuncPtr = std::bind(&MyClass::Add, myClass, std::placeholders::_1, std::placeholders::_2);
	FuncPtr(1, 2);
}

std::function<>

std::function은 다양한 호출 가능한 객체(callable objects)를 캡슐화할 수 있는 범용 래퍼입니다.

함수 포인터, 람다 표현식, 함수 객체 등을 저장할 수 있습니다.

1
std::function<void(int, int)> FuncPtr;

위 문법은 반환 형식이 void이고, 인자가 (int, int)인 형태의 함수 포인터를 선언합니다.

std::bind()

std::function<>으로 만든 함수 포인터는 기본적으로 대입 연산자가 오버로딩 되어있기 때문에 대입이 가능합니다.

1
2
FuncPtr = Add;
FuncPtr(1, 2);

하지만, std::bind 함수도 사용할 수 있는데, 이는 함수의 일부 인자를 고정시키고, 나머지 인자에 대해 호출 가능한 객체를 생성합니다.

1
std::bind("함수_주소", std::placeholders::_1, std::placeholders::_2, ...);

여기에서 std::placeholders는 입력 받는 인자를 뜻합니다.

즉, 미리 인자를 지정할 수도 있지만, std::placeholders::_1 등으로 아직 바인딩되지 않은 인자를 표현할 수 있습니다.

클래스 멤버 함수와 std::bind

클래스의 멤버 함수를 std::bind와 함께 사용할 때는 객체의 인스턴스 또는 포인터도 함께 전달해야 합니다.

위의 경우 std::bindstd::placeholders::_1에 들어갈 객체는 this 포인터가 됩니다.

즉, 아래와 같이 사용할 수 있습니다.

1
std::bind(&MyClass::Add, myClass, std::placeholders::_1, std::placeholders::_2);

&MyClass::Add는 함수 포인터를, myClass는 해당 객체의 this 포인터입니다.

이 글에서 알 수 있듯이 class 객체에서 함수를 호출할 때, 첫 번째 인자로 자기 자신을 가리키는 this 포인터를 함께 보낸다는 것을 다시 한 번 알 수 있습니다.

이에 대한 관련 글은 함수 호출 규약의 thiscall이나 함수 포인터 글의 Class 에서의 함수 포인터 글을 읽어 보시는 것도 좋을 것 같습니다.


참고

[함수 포인터] 01. 함수 포인터와 멤버 함수 포인터

[함수 포인터] 02. 콜백 함수와 델리게이트

[함수 포인터] 03. 일급 객체와 고차 함수

이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.