
직접 부딪히고 공부하며 작성한 코드라 많이 부족한 예제와 설명일 수 있습니다. 틀린 설명이 있거나 더 좋은 방법이 있다면 의견은 언제나 환영입니다!
리플렉션 구현하기의 마지막 포스팅! 추후에 포스팅을 더 하게된다면, 지금까지 구현했던 것들을 개선하고 수정했을 때 일 것 같다. 아무튼, 이번 포스팅의 목표는 멤버 함수 리스트 출력하고 함수 호출하기다. ClassInfo
에 Function
의 정보를 자동으로 등록해주는 매크로는 아래와 같다.
#define FUNCTION(FunctionName, ReturnType, ...) \
inline static struct RegistFunction_##FunctionName \
{ \
RegistFunction_##FunctionName() \
{ \
FunctionRegister<ReturnType (ClassType::*)(__VA_ARGS__), \
&ClassType::FunctionName> functionRegister_##FunctionName(#FunctionName); \
} \
} registFunction_##FunctionName; \
\
변수를 등록하는 매크로와 거의 유사한 것을 볼 수 있다. 실제로 생성되는 함수의 클래스는 다음과 같다.
class Function
{
public:
virtual std::string GetName() const = 0;
virtual void PrintSignature() = 0;
virtual void ProcessEvent(
void* instance,
const std::vector<std::any>& params = {}) = 0;
};
template <typename OwnerClassType, typename ReturnType, typename... Args>
class FunctionHandler : public Function
{
public:
using FuncPtr = ReturnType(OwnerClassType::*)(Args...);
// 함수 정보를 초기화하는 생성자
FunctionHandler(std::string name, FuncPtr funcPtr)
: name(name)
, funcPtr(funcPtr)
, returnType(typeid(ReturnType).name())
, paramTypes{ typeid(Args).name()... }
{
}
// 함수의 이름을 반환하는 Getter
std::string GetName() const override { return name; }
// 자신의 정보를 출력하는 함수
void PrintSignature() override;
// 함수를 호출하는 함수
void ProcessEvent(
void* instance,
const std::vector<std::any>& params = {}) override;
// 실제로 함수 호출을 처리하는 함수
template <size_t... Indices>
void CallFunction(
OwnerClassType* object,
const std::vector<std::any>& params,
std::index_sequence<Indices...>)
private:
std::string name; // 함수 이름
std::string returnType; // 함수 반환값
std::vector<std::string> paramTypes; // 함수 파라미터 타입
FuncPtr funcPtr; // 멤버 함수 포인터
};

리플렉션이 완료된 후, PrintClassInfo()
함수를 호출해 클래스 정보를 호출한 결과이다. 이번 포스팅에서는 화살표로 표시해둔 부분(함수 리스트)을 출력하는 것을 목표로 하고, 함수를 호출하는 것까지 해보려고 한다.
class MyClass : public SuperClass
{
GENERATED_BODY(MyClass)
public:
void Foo() { std::cout << "[Execute] Foo!" << std::endl; }
FUNCTION(Foo, void)
};
함수 밑에 FUNCTION(함수명, 반환타입...)
매크로를 넣어 함수 정보를 ClassInfo
에 자동으로 등록하도록 했다.
void Foo() { std::cout << "[Execute] Foo!" << std::endl; }
inline static struct RegistFunction_Foo {
RegistFunction_Foo() {
FunctionRegister<void (ClassType::*)(), &ClassType::Foo>
functionRegister_Foo("Foo");
}
} registFunction_Foo;
FUNCTION
매크로를 확장해보면 다음과 같다. 변수와 비슷하기 때문에 구체적인 설명은 생략하겠다.
inline static struct RegistFunction_Foo
: 함수를 등록하는 구조체를 정의한다.FunctionRegister<void (ClassType::*)(), &ClassType::Foo> functionRegister_Foo("Foo");
:RegistFunction_Foo()
의 생성자에서 함수 등록기를 만들고,ClassInfo
에 함수를 등록해준다.
함수 등록하기
FunctionRegister
template <typename Signature, Signature FuncPtr>
class FunctionRegister;
template <typename OwnerClassType, typename ReturnType, typename... Args, ReturnType(OwnerClassType::* FuncPtr)(Args...)>
class FunctionRegister<ReturnType(OwnerClassType::*)(Args...), FuncPtr>
{
public:
FunctionRegister(std::string name)
{
FunctionHandler<OwnerClassType, ReturnType, Args...>* function = new FunctionHandler<OwnerClassType, ReturnType, Args...>(name, FuncPtr);
OwnerClassType::GetClass().AddFunction(name, function);
}
};
우선 부분 특수화 부분을 보자.
template <typename OwnerClassType, typename ReturnType, typename... Args, ReturnType(OwnerClassType::* FuncPtr)(Args...)>
class FunctionRegister<ReturnType(OwnerClassType::*)(Args...), FuncPtr>
멤버 함수 포인터는 ReturnType(OwnerClassType::* FuncPtr)(Args...)
이렇게 생겼다. 이 함수가 어느 클래스에 속하는지(OwnerClassType
), 반환하는 타입(ReturnType
)은 어떤 것인지, 함수가 받는 인자의 타입(Args...
)은 무엇인지에 대한 중요한 정보가 들어있는 것이다.
멤버 함수 포인터에서 이러한 중요한 정보들을 자동으로 추출하기 위해 부분 특수화를 사용한다. 즉, 멤버 함수 포인터를 '분해'해서 내부 정보를 쉽게 얻고 활용하기 위한 편리한 방법이라고 보면 된다.
원래는 template <typename OwnerClassType, typename ReturnType, typename... Args, ReturnType(OwnerClassType::* FuncPtr)(Args...)>
이런식으로 모든 타입 정보를 명시적으로 받았었는데, 계속 컴파일 오류가 났었다.

그렇다네요.. 사실 잘은 모르겠습니당..
template <typename Signature, Signature FuncPtr>
class FunctionRegister;
아무튼 이런 문제를 쉽게 해결하는 방법이 부분 특수화였고, 그래서 기본 템플릿이 이런 형식을 띄게 된 것이다. 기본 템플릿은 함수의 시그니처를 나타내는 형식인 Signature
타입과 해당 타입의 함수 포인터인 FuncPtr
를 받는다. "어떤 함수 포인터"를 받을 것인지에 대한 틀만 제공하는 역할을 한다.
FunctionHandler<OwnerClassType, ReturnType, Args...>* function = new FunctionHandler<OwnerClassType, ReturnType, Args...>(name, FuncPtr);
이제 Function
을 만들어 줄 것이다. 특수화 덕분에 분해된(?) 중요한 정보들을 사용한다.
FunctionHandler
template <typename OwnerClassType, typename ReturnType, typename... Args>
class FunctionHandler : public Function
{
public:
using FuncPtr = ReturnType(OwnerClassType::*)(Args...);
FunctionHandler(std::string name, FuncPtr funcPtr)
: name(name)
, funcPtr(funcPtr)
, returnType(typeid(ReturnType).name())
, paramTypes{ typeid(Args).name()... }
{
}
};
FuncPtr
이라는 타입 별칭을 정의하고, 함수의 정보를 초기화해주자. 함수의 이름, 반환형, 매개변수 모두 문자열 형태로 저장한다. 매개변수를 초기화하는 부분( typeid(Args).name()...
)이 조금 난해해보일 수 있는데, 매개변수 팩(Args
)에 포함된 각 타입에 대해 typeid(Args).name()
를 호출해 해당 타입의 이름을 얻어준다고 이해하면 된다.
OwnerClassType::GetClass().AddFunction(name, function);
마지막으로 자신이 포함된 클래스의 ClassInfo
에 방금 생성한 함수의 메타데이터 정보를 저장해주면 된다.
함수 호출하기
Function* FindFunctionByName(std::string name)
{
const auto& iter = functions.find(name);
if (iter != functions.end())
{
return iter->second;
}
else
{
return nullptr;
}
}
ClassInfo
클래스에 FindFunctionByName
함수를 만들어 주었다. 문자열을 받아 함수의 메타데이터를 찾고 반환해주는 함수이다.
Function* foo = MyClass::GetClass().FindFunctionByName("Foo");
if (foo)
{
foo->ProcessEvent(myclass);
}
Function* coo = MyClass::GetClass().FindFunctionByName("Coo");
if (coo)
{
coo->ProcessEvent(myclass, { 3, 5.3f, 6 });
}
이런식으로 먼저 함수의 메타데이터를 찾은 뒤, 클래스 객체와 매개변수 벡터를 보내서 함수를 호출해줄 것이다.
void ProcessEvent(void* instance, const std::vector<std::any>& params = {}) override
{
auto object = reinterpret_cast<OwnerClassType*>(instance);
CallFunction(object, params, std::make_index_sequence<sizeof...(Args)>{});
}
ProcessEvent
함수가 불리면, 먼저 void*
를 자신의 클래스 타입으로 캐스팅해준다. 그 다음 실질적으로 함수를 호출해주는 CallFunction
을 호출하게 되는데, 캐스팅된 인스턴스와 파라미터, 그리고 의문의 std::make_index_sequence<sizeof...(Args)>{}
를 전달하게 된다.
std::make_index_sequence
는 템플릿 매개변수 팩 Args...
에 포함된 매개변수 개수만큼의 인덱스 시퀀스를 생성해준다. 예를 들어, Args...
가 3개의 타입이라면, std::make_index_sequence<3>{}
는 std::index_sequence<0, 1, 2>
와 같다. sizeof...(Args)
는 템플릿 매개변수 팩에 포함된 인자의 개수를 나타내는 친구이므로, Args
에 몇 개의 타입이 전달됐는지를 계산해주는 역할을 한다고 보면 된다.
template <size_t... Indices>
void CallFunction(OwnerClassType* object, const std::vector<std::any>& params, std::index_sequence<Indices...>)
{
(object->*funcPtr)(std::any_cast<Args>(params[Indices])...);
}
(object->*funcPtr)
:funcPtr
은 저장된 멤버 함수 포인터이고,(object->*funcPtr)
은 객체object
에 대해 해당 멤버 함수를 호출하는 표현이다.std::any_cast<Args>(params[Indices])...
:Indices
가 순서대로 0,1,2... 가 되므로, 각 인자에 대해std::any_cast<Args>(params[Indices])
를 호출하게 된다. 만약 Args가 int, float, int였다고 한다면,params
에 있던 0번 인덱스는 int로, 1번 인덱스는 float으로, 2번 인덱스는 int로 any_cast 되는 것이다. 이것을 이용해 모든 인자를 올바른 타입으로 변환하고 함수의 매개변수로 전달하게 된다.
구현 결과
class MyClass : public SuperClass
{
GENERATED_BODY(MyClass)
public:
void Foo()
{
std::cout << "[Execute] Foo!" << std::endl;
}
FUNCTION(Foo, void)
void Boo(int a)
{
std::cout << "[Execute] Boo : " << a << std::endl;
}
FUNCTION(Boo, void, int)
int Coo(int a, float b, int c)
{
std::cout << "[Execute] Coo : " << a << " " << b << " " << c << std::endl;
return 0;
}
FUNCTION(Coo, int, int, float, int)
};
MyClass
에 여러개의 함수를 선언 / 정의 해주고,
int main()
{
Function* foo = MyClass::GetClass().FindFunctionByName("Foo");
if (foo)
{
foo->ProcessEvent(myclass);
}
Function* boo = MyClass::GetClass().FindFunctionByName("Boo");
if (boo)
{
boo->ProcessEvent(myclass, { 3 });
}
Function* coo = MyClass::GetClass().FindFunctionByName("Coo");
if (coo)
{
coo->ProcessEvent(myclass, { 3, 5.3f, 6 });
}
};

함수를 실행시켜보면 잘 실행되는 것을 알 수있다 ᵔᴥᵔ
리플렉션 구현 후기
아직 아쉬운 점이 있다면.. 반환값 받아오는 것을 못했다는 것? 지금 당장은 아니더라도 계속 개선해 나갈 수 있으면 좋겠다. 리플렉션이 처음엔 개념 자체도 감이 안잡혀서 너무 힘들고 어려웠었다. 물론 지금도 아무것도 없이 짜라고 하면 못 짤 것 같지만... 헤헤 ᵔᴥᵔ
그래도 템플릿 프로그래밍, 모던 C++ 등 그동안 애써 외면해왔던 부분들을 마주하고 직접 사용해볼 수 있는 계기가 되어 좋았던 것 같다. 외계어 같은 모던 C++이랑 안면은 튼 느낌. 언리얼이 아아아주 기본적으로 어떤식으로 동작하는지 감이 오기도 하고!
아무튼 C++ 공부도 계속 해야할 것 같고, 이득우 교수님의 언리얼 강의를 다시 들어봐야 겠다는 생각이 들었다.