[함수 포인터] 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
로 만들어 사용할 수 있게 됩니다.
using으로 별칭 만들어 사용하기
C++ 11 이후부터 사용할 수 있는 using
도 typedef
와 비슷하게 사용할 수 있습니다.
다만, 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
이라는 별칭으로 사용할 수 있습니다.
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>
사용하기
typedef
나 using
을 사용하는 것 외에도 <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::bind
의 std::placeholders::_1
에 들어갈 객체는 this
포인터가 됩니다.
즉, 아래와 같이 사용할 수 있습니다.
1
std::bind(&MyClass::Add, myClass, std::placeholders::_1, std::placeholders::_2);
&MyClass::Add
는 함수 포인터를, myClass
는 해당 객체의 this
포인터입니다.
이 글에서 알 수 있듯이 class 객체에서 함수를 호출할 때, 첫 번째 인자로 자기 자신을 가리키는
this
포인터를 함께 보낸다는 것을 다시 한 번 알 수 있습니다.이에 대한 관련 글은 함수 호출 규약의 thiscall이나 함수 포인터 글의 Class 에서의 함수 포인터 글을 읽어 보시는 것도 좋을 것 같습니다.