Delegate
- 게임 엔진 컴포넌트와 랜더러 엔진을 구성하는 도중 객체의 멤버함수나 전역 함수를 같은 형식의 함수를 묶어 특정 이벤트에서 한번에 호출해주는 방식을 도입하면 편리할 것 같아 도입하게 되었다.
- 유니티와 같은 경우에는 Delegate 타입으로 함수포인터 변수를 선언하여 해당 타입을 통해 이벤트를 등록해두어 사용할 수 있게 되어있다.
- 언리얼 같은 경우엔 Delegate 타입을 Delegate 전용 define을 통해 미리 선언해 두어야 하는데 해당 define은 함수의 종류에 따라 각각 호출하는 함수(Bind, BindStatic, BindRaw, BindSP, BindUObject ...)가 다르다.
- 일단 현재 프로젝트에서 간단히 같은 형식의 함수를 묶어 호출해주는 방식과, 해당 함수의 오버라이딩 체크를 할 수 있는 기능만 구현해보려고 했다.
Delegate Variadic Template Class
//***************************************************************************************
//
// Delegate Class
//
//***************************************************************************************
template<typename ..._Args>
class Delegate
{
public:
using Function = std::function<void(_Args...)>;
public:
inline void Push(Function&& _func); // Event Function 추가..
inline void Pop(Function&& _func); // Event Function 제거..
inline void Reset(); // Event Function 초기화..
public:
inline void operator+=(const Function& _func); // Event Function 추가..
inline void operator+=(const Function&& _func); // Event Function 추가..
inline void operator-=(const Function& _func); // Event Function 제거..
inline void operator-=(const Function&& _func); // Event Function 제거..
inline void operator =(const Function& _func); // Event Function 초기화 후 추가..
inline void operator()(_Args... _types); // Event Function List 호출..
private:
std::vector<Function> pFunctionList;
};
Delegate Class 제작시 필요하다 생각한 기능
1. 해당 함수들의 매개변수가 동일하다면 같은 클래스를 활용하여 특정 이벤트에서 한번에 호출하기 쉽도록 한다.
2. 현재 게임 엔진 컴포넌트 함수 포인터 등록과 랜더러 측 패스별 함수 포인터 등록 부분에 상위 클래스로부터 해당 함수가 오버라이딩 되었는지 체크를 할 수 있어야 한다.
3. 컴파일 타임에 Delegate에 맞지 않는 함수를 삽입, 삭제하는 경우와 함수 호출 시 등록해둔 함수의 매개변수와 일치하지 않는 경우를 체크할 수 있어야한다.
Function Pointer Binding에 대한 고찰
C++11 std::bind를 활용한 Function Binding
- 처음엔 원래 사용하던 std::bind 함수를 Delegate Class 함수 내부에서 묶어주는 형식으로 하려 하였지만 여러가지 문제점이 존재하였다.
- 가장 큰 문제점은 해당 Delegate Class에 들어오는 함수의 바인딩 가능 여부를 함수 내부를 들어와서 std::bind 함수를 통해 Binding 가능 여부를 판단할 수 있어, 어느 부분에서 잘못 넣어주었는지 유추하기 어렵다.
- 다른 문제점은 함수의 매개변수 개수 및 위치에 따른 std::placeholders를 통해 명시해주어야 하는 것인데, 어짜피 가변인자를 재귀 방식을 통해 설정해주면 되긴 하지만 이것 또한 범용적으로 사용하기에 불편함이 있다고 생각하였다.
C++20 std::bind_front를 활용한 Function Binding
- std::bind를 사용할 경우 해당 함수의 매개변수에 따른 추가 std::placeholders를 자동화 할 방법이 없을까 찾아보던 도중 std::invoke를 통해 한번 랩핑을 하여 해당 함수 포인터를 반환하는 방식을 찾았었다. 결과적으론 해당 기능은 C++20 스펙에 std::bind_front를 통해 제공하고 있었으며, 이를 활용하여 적용해보려고 하였지만 문제점이 아직 해결되지 않았다.
- 일단 std::bind를 활용하여 해당 매개변수의 개수 및 위치에 따른 추가 작업을 할 필요없이 범용적으로 사용할 수는 있게 되었지만, 이 방식 또한 함수 내부에서 바인딩을 하는 방식이기에 컴파일 타임에 사용자가 넣은 구문에서의 컴파일 에러가 아닌 바인딩 함수 호출 구문에서의 컴파일 에러가 나기 때문에 사용할 수 없다고 판단되었다.
Lambda를 활용한 Function Binding
- 최종적으로 현재 적용중인 바인딩 방식이다. 생각보다 원리는 단순하다. 해당 함수를 래핑하는 함수를 지원하여 std::function을 통해 매개변수로 받아 Delegate에 해당 함수를 등록하는 방식이다.
- 처음엔 크게 두가지 방식의 바인딩 함수를 지원하려고 하였다. 전역성을 띄는 함수와 특정 객체의 멤버 함수를 바인딩하는 함수를 오버로드하여 다르게 사용하도록 유도하려고 하였지만, 위에서 말했듯 우리 프로젝트에선 함수의 오버라이딩 체크가 필요한데 이를 체크하기 위해선 두가지의 함수만으론 불가능해 오버라이딩이 안된 객체의 함수인 경우 해당 상위 객체로 캐스팅하여 함수를 호출하는 람다함수를 반환하는 방식의 함수를 추가하였다.
//***************************************************************************************
//
// Bind Function Template
//
//***************************************************************************************
template<typename B, typename T>
using Base_Check = typename std::enable_if<std::is_base_of<B, T>::value, bool>::type;
// Override 된 함수가 없는 경우 해당 부모의 함수 호출하는 예외적인 함수..
// 상속 관계에서 Overriding 체크할 경우 예외가 나오기에 추가..
template<typename R, typename T, typename B, typename... Args, Base_Check<T, B> = NULL>
constexpr auto Bind(R(T::* f)(Args...), B* p)
{
return [p, f](Args... args) ->R { return (static_cast<T*>(p)->*f)(args...); };
};
// 기본적인 Member Function을 Lambda Function으로 묶어주는 함수..
template<typename R, typename T, typename... Args>
constexpr auto Bind(R(T::* f)(Args...), T* p)
{
return [p, f](Args... args) ->R { return (p->*f)(args...); };
};
// 기본적인 Static Function을 Lambda Function으로 묶어주는 함수..
template<typename R, typename... Args>
constexpr auto Bind(R(*f)(Args...))
{
return [f](Args... args) ->R { return (*f)(args...); };
};
왜 지원하는 bind 함수를 안쓰고 Lambda 함수를 통해 만들었는가?
- 사실 std::bind 같은 경우는 안전성이 보장되어 있고 증명이 되어있지만, 그 만큼 프로젝트에서 속도 측면에서 큰 이득을 보기 힘들었다. 처음 함수포인터를 통한 오버라이딩 체크를 하여 해당 컴포넌트들의 함수를 오버라이딩 한 경우에만 호출하는 방식이 빠를 것이라 예상되어 도입하였지만 속도 체크를 안해본 우리의 실수였던 것 같다.
각각의 함수 Binding 사용 방식
int g_Int = 0;
float g_Float = 0.0f;
class Member
{
private:
int m_Int = 0;
float m_Float = 0.0f;
public:
void Function(int _1, float _2)
{
m_Int += _1;
m_Float += _2;
}
};
void g_Function(int _1, float _2)
{
g_Int += _1;
g_Float += _2;
}
int main(void)
{
// Lambda
auto _lam1 = Bind(g_Function);
auto _lam2 = Bind(&Member::Function, &mem);
// std::bind
auto _bind1 = std::bind(g_Function, std::placeholders::_1, std::placeholders::_2);
auto _bind2 = std::bind(&Member::Function, &mem, std::placeholders::_1, std::placeholders::_2);
// std::bind_front
auto _bind_front1 = std::bind_front(g_Function);
auto _bind_front2 = std::bind_front(&Member::Function, &mem);
}
std::bind, std::bind_font, Lambda 속도 측정
- Lambda를 활용하여 만들고 나서 실질적으로 속도 측정을 해봤을때 라이브러리 함수보다 느리다면 사용할 이유가 없을 것 같아 속도 측정을 해보았더니 생각보다 많은 속도 차이가 나게 되는것을 보았다.
- 각각 바인딩 함수별 전역 함수와 객체의 멤버 함수 두 함수를 테스트 회수만큼 호출 했을때의 시간 측정 결과이다.
- 현재 오버라이딩 체크 및 특정 이벤트에서의 함수 리스트 호출, UI Button에서 마우스 클릭 이벤트 처리 방식을 해당 Delegate로 활용하고 있다.
- 일단 속도 측면에서도 이득을 보았고, 추가적인 예외상황으로 인한 오류가 있을 경우 수정하여 사용할 예정이다.
'C++' 카테고리의 다른 글
[C++] Return Type Operator (0) | 2021.12.20 |
---|---|
[C++] Define Macro (0) | 2021.12.19 |
[C++] std::enable_if / std::is_base_of (0) | 2021.12.19 |