KoreanFoodie's Study
[C++ 게임 서버] 6-3. JobQueue #2 본문
[C++ 게임 서버] 6-3. JobQueue #2
핵심 :
1. Job 별로 클래스 상속을 무한히 늘려나가기 보다, Functor 와 Tuple 을 이용해 각 Job 을 간편하게 추가해 보자.
2. C++ 17 에서는 std::apply 를, C++ 11 에서는 Template Meta Programming 을 이용해 특정 Functor 에 임의의 갯수의 인자를 넘겨주는 기능을 구현할 수 있다.
저번 시간에는 Job 과 JobQueue 에 대한 개념을 간단하게 소개했다.
그런데, 사실 Job 이 늘어날 때마다 이를 상속해서 클래스를 무한히 늘려나가는 방식은... Job 의 갯수가 늘어난다고 하면 매우 끔찍한 결과를 초래할 수 있다.
Job 을 추가하는 과정을 조금 간략화하기 위해, 템플릿을 이용해 각 Job 에 해당하는 동작을 Functor 로 저장할 수 있도록 만들면 어떨까? 🤔
C++ 17 에서 사용 가능한 아래 예제 코드를 보자.
// 함수자 (Functor)
class IJob
{
public:
virtual void Execute() { }
};
template<typename Ret, typename... Args>
class FuncJob : public IJob
{
using FuncType = Ret(*)(Args...);
public:
FuncJob(FuncType func, Args... args) : _func(func), _tuple(args...)
{
}
virtual void Execute() override
{
std::apply(_func, _tuple); // C++17
}
private:
FuncType _func;
std::tuple<Args...> _tuple;
};
template<typename T, typename Ret, typename... Args>
class MemberJob : public IJob
{
using FuncType = Ret(T::*)(Args...);
public:
MemberJob(T* obj, FuncType func, Args... args) : _obj(obj), _func(func), _tuple(args...)
{
}
virtual void Execute() override
{
std::apply(_func, _tuple); // C++17
}
private:
T* _obj;
FuncType _func;
std::tuple<Args...> _tuple;
};
먼저, Execute 를 호출할 IJob 인터페이스를 만들고, FuncJob 과 MemberJob 이 이를 상속받도록 한다.
FuncJob 부터 살펴보면... FuncType 의 Functor 와 tuple 을 받는 녀석인데, 이 때 Functor 는 해당 Job 의 실제 동작에 대한 함수를, tuple 은 해당 Functor 에 쓰일 인자를 의미한다.
std::apply 를 사용하면, 해당 Functor 에 tuple 을 대입해 실행 시킬 수 있는데, tuple 을 사용한 이유는... 임의의 갯수의 인자를 받아서 사용하고 싶은데, 멤버 변수로 Args... _args 같은 문법을 사용하는 것은 C++ 에서 지원하지 않기 때문이다! 🤣
MemberJob 은 그냥 클래스 멤버 함수를 Functor 로 사용할때 해당 클래스의 객체를 넣어서 사용할 것임을 파악할 수 있다.
위 방식을 사용하면, Room 클래스에서 PushJob 함수는 다음과 같이 간략화된다 :
template<typename T, typename Ret, typename... Args>
void PushJob(Ret(T::*memFunc)(Args...), Args... args)
{
auto job = MakeShared<MemberJob<T, Ret, Args...>>(static_cast<T*>(this), memFunc, args...);
_jobs.Push(job);
}
이제 job 은 Room 의 Member 함수를 받아 JobQueue 에 넣어줄 것이다.
이제 ClientPacketHandler 에서는, 아래와 같이 채팅 패킷을 Handling 하게 된다!
bool Handle_C_ENTER_GAME(PacketSessionRef& session, Protocol::C_ENTER_GAME& pkt)
{
/** 기존과 동일 */
//GRoom.PushJob(MakeShared<EnterJob>(GRoom, player));
GRoom.PushJob(&Room::Enter, player);
/** 기존과 동일 */
return true;
}
bool Handle_C_CHAT(PacketSessionRef& session, Protocol::C_CHAT& pkt)
{
/** 기존과 동일 */
//GRoom.PushJob(MakeShared<BroadcastJob>(GRoom, sendBuffer));
GRoom.PushJob(&Room::Broadcast, sendBuffer);
/** 기존과 동일 */
return true;
}
그럼 C++ 17 이전에는 어떻게 구현했을까? 이를 위해서는 템플릿 흑마법을 조금 살펴보아야 한다...
// C++11 apply
template<int... Remains>
struct seq
{};
template<int N, int... Remains>
struct gen_seq : gen_seq<N-1, N-1, Remains...>
{};
template<int... Remains>
struct gen_seq<0, Remains...> : seq<Remains...>
{};
template<typename Ret, typename... Args>
void xapply(Ret(*func)(Args...), std::tuple<Args...>& tup)
{
xapply_helper(func, gen_seq<sizeof...(Args)>(), tup);
}
template<typename F, typename... Args, int... ls>
void xapply_helper(F func, seq<ls...>, std::tuple<Args...>& tup)
{
(func)(std::get<ls>(tup)...);
}
// 함수자 (Functor)
class IJob
{
public:
virtual void Execute() { }
};
template<typename Ret, typename... Args>
class FuncJob : public IJob
{
using FuncType = Ret(*)(Args...);
public:
FuncJob(FuncType func, Args... args) : _func(func), _tuple(args...)
{
}
virtual void Execute() override
{
//std::apply(_func, _tuple); // C++17
xapply(_func, _tuple);
}
private:
FuncType _func;
std::tuple<Args...> _tuple;
};
음.. 사실 처음 봤을 때는 무슨 일이 일어나는지 전혀 감이 오질 않는다. 🤪
일단 xapply 부터 차근히 순서를 따라가보자. 이 녀석은 아래 부분을 호출하는데...
xapply_helper(obj, func, gen_seq<sizeof...(Args)>(), tup);
gen_seq<sizeof...(Args)> 는 Args 의 갯수 만큼을 받아 템플릿화한다. 즉, 인자(Args)로 들어온 것이 3개일 경우, gen_seq<3> 같은 식으로 뱉을 것이다.
그리고 나서 gen_seq 쪽을 보면, gen_seq 는 gen_seq<N-1, N-1, Remains...> 를 상속 받는데, 상속 구조를 정리해보면 아래와 같을 것이다 :
auto s = gen_seq<3>();
// gen_seq<3>
// : gen_seq<2, 2>
// : gen_seq<1, 1, 2>
// : gen_seq<0, 0, 1, 2>
// : seq<0, 1, 2>
위와 같이 계속 상속구조가 생기다가, 첫 인자가 0 이 되면 seq<Remains...> 를 상속받게 되면서 재귀가 종료될 것이다.
그럼 다시 xapply_helper 의 구현부를 보면,
template<typename F, typename... Args, int... ls>
void xapply_helper(F func, seq<ls...>, std::tuple<Args...>& tup)
{
(func)(std::get<ls>(tup)...);
}
seq 타입을 받는 것을 확인할 수 있다. 즉, func 는 seq<0, 1, 2> 를 시작으로 받으면서, std::get 을 통해 순서대로 <0>, <1>, <2> 를 인자로 받을 것임을 유추할 수 있다. 아래처럼 말이다.
auto tup = std::tuple<int32, int32>(1, 2);
auto val0 = std::get<0>(tup);
auto val1 = std::get<1>(tup);
멤버함수를 사용하는 경우에는, 다음과 같은 코드를 추가해 주면 될 것이다!
template<typename T, typename Ret, typename... Args>
void xapply(T* obj, Ret(T::*func)(Args...), std::tuple<Args...>& tup)
{
xapply_helper(obj, func, gen_seq<sizeof...(Args)>(), tup);
}
template<typename T, typename F, typename... Args, int... ls>
void xapply_helper(T* obj, F func, seq<ls...>, std::tuple<Args...>& tup)
{
(obj->*func)(std::get<ls>(tup)...);
}
template<typename T, typename Ret, typename... Args>
class MemberJob : public IJob
{
using FuncType = Ret(T::*)(Args...);
public:
MemberJob(T* obj, FuncType func, Args... args) : _obj(obj), _func(func), _tuple(args...)
{
}
virtual void Execute() override
{
//std::apply(_func, _tuple); // C++17
xapply(_obj, _func, _tuple);
}
private:
T* _obj;
FuncType _func;
std::tuple<Args...> _tuple;
};
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 6-5. JobQueue #4 (0) | 2023.12.20 |
---|---|
[C++ 게임 서버] 6-4. JobQueue #3 (0) | 2023.12.20 |
[C++ 게임 서버] 6-2. JobQueue #1 (1) | 2023.12.19 |
[C++ 게임 서버] 6-1. 채팅 실습 (0) | 2023.12.19 |
[C++ 게임 서버] 5-9. 패킷 자동화 #2 (0) | 2023.12.18 |