상세 컨텐츠

본문 제목

[C++11] 멤버 함수 포인터를 일반 함수 포인터로 바꾸기

프로그래밍/테크닉

by 적분 ∫2tdt=t²+c 2021. 8. 8. 17:39

본문

C++로 개발을 하다보면 종종 C API를 호출해야하는 경우가 있습니다. 많은 유용한 라이브러리나 OS 등이 C API만 제공하고 C++스타일 API를 제공하지 않기 때문인데요, C++ 철학에 맞춰 코드를 짜다가도 이렇게 C API를 호출하려하다보면 코드가 쉽게 지저분해지곤 합니다. C스타일로 작업하면 특히 지저분해지기 쉬운게 자원 획득 & 해제와 콜백 함수 넘기는 작업입니다. 이 중 자원 획득 & 해제는 unique_ptr나 shared_ptr의 custom deleter를 이용하면 어느정도 깔끔하게 처리할 수 있으니, 이번 포스팅에서는 콜백함수를 넘기는 작업에 대해 고민해보려고 합니다.

C에서의 콜백함수

콜백함수(Callback)는 나중에 호출하라고 넘겨주는 함수를 가리킵니다. 보통 다음과 같은 형태로 이벤트 처리 등을 위해 널리 쓰이죠. 

여기서 keyevent_cb_t는 나중에 키가 눌렸을때 호출될 함수를 넘기기 위한 함수 포인터 타입이고, register_keyevent는 이 함수포인터를 등록하는 API함수입니다. 보통 여러 콜백이 등록되어 호출될 수 있으므로 현재 콜백함수가 어떤 대상을 처리하는지 알수 있도록 void* userdata를 함께 넘겨받도록 하는게 일반적입니다. 따라서 콜백함수 측에서는 userdata를 이용해서 현재 발생한 이벤트에 대해(위 예시에서는 키 입력) 적절한 처리를 수행할 수 있겠죠.

위의 예시에서는 userdata로 o1 객체에는 "o1"의 포인터를, o2객체에는 "o2"의 포인터를 넘겨주었습니다. 따라서 o1객체에 키입력이 발생하면 on_keydown함수가 호출되어 "o1 received KeyDown[32]", o2객체에 키입력이 발생하면 "o2 received KeyDown[32]"와 같이 메세지가 출력될것입니다.

C++ & 객체지향에서의 콜백함수

C++에서도 C와 마찬가지로 콜백함수와 그 API들을 사용할 수 있습니다. 근데 그냥 C방식대로 쓸거면 그냥 C를 쓰지 C++를 쓸 필요가 없겠죠? C++에서는 특정 객체와 관련된 작업 & 데이터들을 모아서 클래스의 형태로 만들고 이를 유용하게 재사용하는 객체지향이라는 좋은 패러다임이 있습니다.

C++ 개발자들은 위와 같이 MyObject라는 클래스를 생성하여 object를 생성하고 관련 콜백을 등록하는 작업을 클래스 인스턴스의 생성과 연동시키고 싶어할 겁니다. 그리고 키가 눌려서 on_keydown 함수가 호출되면 그냥 자기의 멤버 변수 및 멤버 함수들을 활용해 하고 싶은일을 편하게 할 수 있는 아름다운 구조가 될테니깐요. 더 나아가 on_keydown 함수를 virtual로 하면 MyObject를 상속받은 클래스들에서는 쉽게 on_keydown의 동작을 확장/수정할 수 있습니다.

근데 그러려면 register_keyevent에 콜백함수로 어떤 녀석을 던져줘야 할까요? 일단 멤버 함수의 포인터는 던져줄 수가 없습니다. 그래서 일반적으로 userdata에 this를 넘겨주고 static 멤버 함수로 래핑해서 다음과 같이 구현합니다.

나쁘지 않은 방법입니다. static 멤버 함수는 사실 그냥 일반 함수와 다를바 없으므로 함수 포인터를 뽑아쓸 수 있거든요. 그런데 이렇게 등록할 콜백함수가 한 둘이 아니라면 매번 static 멤버 함수를 만들어야하니 여간 귀찮은 일이 아닙니다. 그냥 &MyObject::on_keydown가 적당히 함수포인터로 바뀌어서 C API함수에게 전달되게 할수는 없을까요?

C++11과 템플릿을 이용한 멤버 함수 포인터의 변환

최근 이거 가지고 열심히 고민하던 끝에 좋은 해결책을 발견했습니다. (당연히 그랬으니 포스팅을 하고 있겠죠~~ㅎㅎ) 템플릿의 파라미터로 타입뿐만 아니라 여러 상수도 넘겨줄 수 있는데, 그 중 포인터와 멤버 포인터도 가능하다는 사실과 C++11의 람다 함수는 캡처한 변수가 없을때 자동으로 함수 포인터로 변환가능하다는 사실을 이용하면 보일러플레이트를 크게 줄일 수 있습니다. 아이디어는 다음과 같아요~

위와 같이 template 파라미터로 임의의 멤버 함수 포인터를 받을 수 있습니다. 그리고 람다 함수에서 이 멤버 함수 포인터를 호출하는 래퍼 함수를 구현해주면 언제나 어디서나 MyObject내의 void를 반환하는 (int)인자 함수를 함수 포인터로 변환할 수 있습니다.

근데 일반성이 좀 부족합니다. 오로지 void를 리턴하는 MyObject 클래스의 멤버함수에만 쓸 수 있으니깐요. 임의의 클래스 및 임의의 리턴 타입에 대해서 동작하도록 개선할 수 없을까요~?

임의의 클래스와 리턴 타입에 대해 사용하도록 확장한 대가로 이제 to_func_ptr사용시 클래스 타입과 리턴 타입을 명시해야합니다. 그리고 하는 김에 리턴 함수 포인터 타입은 Ret(int, Class*)가 되도록 고쳤습니다. 원 타입인 Class*를 살려주는게 좀더 안전하니깐요. 이걸 C API로 넘기기 위해 Ret(int, void*)로 바꾸는건 뭐 식은죽 먹기구요. 반복적으로 클래스 타입과 리턴 타입을 명시하는게 너무 귀찮으므로 이를 없애봅시다. 함수 포인터가 주어지면 자동으로 클래스 타입과 리턴 타입을 추정하도록 할 수 있을까요?

멤버 함수 포인터의 소속 클래스와 리턴 타입을 구해주는 도우미 함수를 선언했습니다. 실제 구현은 전혀 필요없는 함수인게 재밌습니다. 그저 컴파일러에게 타입 추론을 시키기 위해서만 쓰이는 녀석이죠. 이걸 이용해 임의의 멤버 함수 포인터가 입력되더라도 그 멤버 함수 포인터의 소속 클래스와 리턴 타입을 쉽게 알아낼 수 있습니다. 이를 이용해 TO_FUNC_PTR 매크로를 만들었구요. 이제 TO_FUNC_PTR(&MyObject::on_keydown)이라고 쓰면 이는 자동적으로 void(*)(int, MyObject*)타입의 함수 포인터로 치환되는 셈입니다. 아주 유용하겠죠?

임의 인자에 대해서도 적용하기

위에서는 입력 인자가 int하나인 경우에 대해서만 TO_FUNC_PTR를 정의했었는데요, 이를 임의 개수의 인자로 확장할 수 있을까요~? 이건 사실 좀 복잡합니다. (작동하는 코드 샘플은 https://godbolt.org/z/jYGY8Eafv 에서 보실 수 있습니다.)

좀 복잡해보이지만 위에서 다뤘던 내용에 임의 개수의 인자를 적용한 것뿐입니다. 인자 여러개를 리턴 타입으로 주고 받을 수는 없으니 이를 tuple로 묶어서 다루기 위해 부분 특수화 트릭을 사용한 것이죠.

위와 같이 임의의 클래스의 멤버 함수에 대해서 쉽게 적용이 가능합니다. 위 코드는 C++11이 지원되는 gcc 4.8 이상이나 clang 3.4이상에서 문제 없이 컴파일됩니다. 이 트릭을 잘 사용한다면 이제 C API 연동시 번거롭게 static 멤버 함수 래핑을 사용할 필요가 없겠죠? 이걸 적용했더니 현재 개인적으로 진행중인 프로젝트의 라인수가 또 줄어들고 말았다는 사실...! 모두들 적게 코딩하시고 손가락 건강한 삶 사시길 바라는 마음에서 공유드렸습니다~!

태그

관련글 더보기

댓글 영역

  • 프로필 사진
    2021.08.19 13:22
    조금 다른 방식으로 구현해봤습니다.

    함수를 분해할 수 있는 TypeDecomposer를 만들고 거기서 C 함수 포인터를 반환하는 방식입니다.

    https://godbolt.org/z/7cnMorh8M