KoreanFoodie's Study
[C++ 게임 서버] 2-10. TypeCast 본문
[C++ 게임 서버] 2-10. TypeCast
핵심 :
1. 템플릿 메타 프로그래밍(TMP)로 TypeCast 를 구현해 보자. static_cast 를 이용해 dynamic_cast 의 효과를 낼 수 있다!
2. TMP 를 사용하면, 컴파일 타임에 타입과 관련된 값을 미리 알 수 있다.
3. TypeCast 가 가능한지 여부를 행렬에 저장하여 사용할 것이므로 속도가 매우 빠르며, shared_ptr 에도 사용할 수 있다!
제목은 TypeCast 지만, 오늘은... 즐거운 흑마법 시간이다!
흑마법... 아니, 템플릿 메타 프로그래밍(Template Meta Programming; 줄여서 TMP 라고 할 것)으로, 다양한 타입 간의 Casting 을 직접 만들어 볼 것이다.
왜.. 그런 것을 하냐고? 왜냐니... 그야...
라는 이유도 있지만, 사실 좀 더 그럴듯한 이유도 있다.
예를 들어, 우리가 RPG 게임을 만든다고 해 보자. 그럼 타입으로 Player 도 있고, Mage 도 있고, Knight 도 있고... Monster 도 있을 것이다.
이때, Player 는 Mage 와 Knight 의 부모 클래스이므로, Mage 와 Knight 에서 Player 로의 Casting 은 유효해야 한다. 하지만 그 반대는 성립하면 안되며, Player 에서 Monster 로의 변환도 성립하면 안 될 것이다.
그냥 dynamic_cast 를 쓰면 해결되는 문제가 아니냐? 라고 할 수 있지만... 그러려면 RTTI 를 써야 하는데, 일단 dynamic_cast 는 느리다는 단점이 있다. RTTI 를 키면 좀 무거워지기도 하고.
그래서 실제로는 static_cast 를 쓰는데... 그럴때 TypeCast 템플릿을 만들어 놓으면, static_cast 로 마치 dynamic_cast 를 하는 것 같은 효과를 낼 수 있다.
그래서 흑마법이라는 것이다 😂
이제 잡소리가 길었으니, 코드를 보자. 일단 흑마법 기초를 다시 되새기면서 시작하겠다 😁
#pragma region TypeList
template<typename... T>
struct TypeList;
template<typename T, typename U>
struct TypeList<T, U>
{
using Head = T;
using Tail = U;
};
template<typename T, typename... U>
struct TypeList<T, U...>
{
using Head = T;
using Tail = TypeList<U...>;
};
#pragma endregion
일단, 타입리스트를 위와 같이 정의할 것이다. Variadic Template 을 이용해, 리스트를 컴파일 타임에 만들어 낼 것이다.
대충 아래와 같이 사용할 수 있다 :
TypeList<Mage, Knight>::Head whoAMI;
TypeList<Mage, Knight>::Tail whoAMI2;
TypeList<Mage, TypeList<Knight, Archer>>::Head whoAMI3;
TypeList<Mage, TypeList<Knight, Archer>>::Tail::Head whoAMI4;
TypeList<Mage, TypeList<Knight, Archer>>::Tail::Tail whoAMI5;
TMP 가 좋은 점은, 컴파일 타임에 연산을 하므로 에디터 상에서도 값을 확인할 수 있다는 것이다.
리스트에서 보통 사용하는 게 길이, Index 조회, Find 정도이니, 우리도 마찬가지로 아래와 같이 구현할 것이다 :
#pragma region Length
template<typename T>
struct Length;
template<>
struct Length<TypeList<>>
{
enum { value = 0 };
};
template<typename T, typename... U>
struct Length<TypeList<T, U...>>
{
enum { value = 1 + Length<TypeList<U...>>::value };
};
#pragma endregion
#pragma region TypeAt
template<typename TL, int32 index>
struct TypeAt;
template<typename Head, typename... Tail>
struct TypeAt<TypeList<Head, Tail...>, 0>
{
using Result = Head;
};
template<typename Head, typename... Tail, int32 index>
struct TypeAt<TypeList<Head, Tail...>, index>
{
using Result = typename TypeAt<TypeList<Tail...>, index - 1>::Result;
};
#pragma endregion
#pragma region IndexOf
template<typename TL, typename T>
struct IndexOf;
template<typename... Tail, typename T>
struct IndexOf<TypeList<T, Tail...>, T>
{
enum { value = 0 };
};
template<typename T>
struct IndexOf<TypeList<>, T>
{
enum { value = -1 };
};
template<typename Head, typename... Tail, typename T>
struct IndexOf<TypeList<Head, Tail...>, T>
{
private:
enum { temp = IndexOf<TypeList<Tail...>, T>::value };
public:
enum { value = (temp == -1) ? -1 : temp + 1 };
};
#pragma endregion
각각 Lenght, TypeAt, IndexOf 라는 이름으로 구현했다.
실제 예시는 아래와 같다 :
int32 len1 = Length<TypeList<Mage, Knight>>::value;
int32 len2 = Length<TypeList<Mage, Knight, Archer>>::value;
using TL = TypeList<class Player, class Mage, class Knight, class Archer>;
// 3*3
TypeAt<TL, 0>::Result whoAMI6;
TypeAt<TL, 1>::Result whoAMI7;
TypeAt<TL, 2>::Result whoAMI8;
int32 index1 = IndexOf<TL, Mage>::value;
int32 index2 = IndexOf<TL, Archer>::value;
int32 index3 = IndexOf<TL, Dog>::value;
위에서, TL 의 경우 Player, Mage, Knight, Archer 로 이루어진 4 개의 타입 리스트를 의미한다.
그럼 이제 본격적으로 타입 캐스팅을 위한 준비를 해 보자.
#pragma region Conversion
template<typename From, typename To>
class Conversion
{
private:
using Small = __int8;
using Big = __int32;
static Small Test(const To&) { return 0; }
static Big Test(...) { return 0; }
static From MakeFrom() { return 0; }
public:
enum
{
exists = sizeof(Test(MakeFrom())) == sizeof(Small)
};
};
먼저 Conversion 이다. 음.. 결국 중요한 건 exists 쪽인데, MakeFrom() 을 잘 보자. 이 녀석은 반환형이 From 인데, Test 를 보면 To 를 인자형으로 받는다.
이게 뭐지? 싶지만, From 에서 To 로의 변환이 가능하면 (static_cast 방식으로), Test 의 결과가 Small 타입을 반환할 것이고, 그게 아니라면 Big 타입을 반환할 것이다. "(...)" 이 붙어 있는 것은, To 이외의 모든 경우를 체크하겠다는 것과 같다. 대신 우선순위는 To 가 먼저 가져가지만.
어쨋든, Small 과 Big 의 타입은 각각 __int8, __int32 이니, sizeof 를 통해 비교하면 무조건 다를 것이다. 즉, Test 의 반환형이 Small 이면 Conversion 이 가능하고, Big 이면 Conversion 이 불가능하다는 결과를 유추할 수 있다.
예시를 보면...
bool canConvert1 = Conversion<Player, Knight>::exists;
bool canConvert2 = Conversion<Knight, Player>::exists;
bool canConvert3 = Conversion<Knight, Dog>::exists;
처럼 사용을 하는데, 마우스 오버를 해 보면 이미 값이 계산된 것을 알 수 있다.
Player 에서 Knight, 혹은 Knight 에서 Dog 로의 변환은 exists 값이 0 이 될 것이다.
그럼.. 이제 대망의 타입 캐스팅 쪽을 보자.
template<int32 v>
struct Int2Type
{
enum { value = v };
};
template<typename TL>
class TypeConversion
{
public:
enum
{
length = Length<TL>::value
};
TypeConversion()
{
MakeTable(Int2Type<0>(), Int2Type<0>());
}
template<int32 i, int32 j>
static void MakeTable(Int2Type<i>, Int2Type<j>)
{
using FromType = typename TypeAt<TL, i>::Result;
using ToType = typename TypeAt<TL, j>::Result;
if (Conversion<const FromType*, const ToType*>::exists)
s_convert[i][j] = true;
else
s_convert[i][j] = false;
MakeTable(Int2Type<i>(), Int2Type<j + 1>());
}
template<int32 i>
static void MakeTable(Int2Type<i>, Int2Type<length>)
{
MakeTable(Int2Type<i + 1>(), Int2Type<0>());
}
template<int j>
static void MakeTable(Int2Type<length>, Int2Type<j>)
{
}
static inline bool CanConvert(int32 from, int32 to)
{
static TypeConversion conversion;
return s_convert[from][to];
}
public:
static bool s_convert[length][length];
};
template<typename TL>
bool TypeConversion<TL>::s_convert[length][length];
일단, 여러모로.. 타입 리스트를 이용해 타입 테이블(?)을 만들어 주기 위해 TypeConversion 클래스를 정의했다.
잘 보면, MakeTable 을 통해 타입을 변환가능한지 여부를 s_convert 행렬에 저장을 한다. MakeTable 이 여러 버전이 있는데, 이는 for-loop 을 TMP 방식으로 만들기 위해 오버로딩 가능한 여러 버전을 만든 것이다.
또한 for-loop 을 돌기 위해 템플릿 타입을 마치 숫자인 것처럼 변환을 해 주어야 하는데, 이를 위해 Int2Type 템플릿 클래스를 만들었다. 이 녀석은 그냥 값을 타입처럼 사용할 수 있게 도와준다. 이제, TypeConversion 클래스의 CanConvert 함수를 활용해 캐스팅 가능 여부를 체크할 것이다 😉
이제 진짜 마지막이다. TypeCast 를 보자.
template<typename To, typename From>
To TypeCast(From* ptr)
{
if (ptr == nullptr)
return nullptr;
using TL = typename From::TL;
if (TypeConversion<TL>::CanConvert(ptr->_typeId, IndexOf<TL, remove_pointer_t<To>>::value))
return static_cast<To>(ptr);
return nullptr;
}
template<typename To, typename From>
bool CanCast(From* ptr)
{
if (ptr == nullptr)
return false;
using TL = typename From::TL;
return TypeConversion<TL>::CanConvert(ptr->_typeId, IndexOf<TL, remove_pointer_t<To>>::value);
}
TypeCast 는, 우리가 쌓아올린 CanConvert 를 통해 static_cast 를 수행한다. 물론, 이 녀석은 실질적으로는 dynamic_cast 와 거의 흡사하게 동작한다.
CanCast 는, 캐스팅이 가능한지 여부만을 체크해서 bool 값을 리턴한다.
그런데 코드를 잘 보면... ptr->_typeId 가 있다. 즉, 우리가 구현하고자 하는 녀석들의 클래스 내부에는 _typeId 가 선언되어 있어야 한다.
매번 선언하기 귀찮으니, #define 을 활용하자 :
#define DECLARE_TL using TL = TL; int32 _typeId;
#define INIT_TL(Type) _typeId = IndexOf<TL, Type>::value;
그럼 이제 실제 클래스에는 다음과 같이 쓰면 된다! 😄
using TL = TypeList<class Player, class Mage, class Knight, class Archer>;
class Player
{
public:
Player()
{
INIT_TL(Player);
}
virtual ~Player() { }
DECLARE_TL
};
class Knight : public Player
{
public:
Knight() { INIT_TL(Knight); }
};
class Mage : public Player
{
public:
Mage() { INIT_TL(Mage); }
};
class Archer : public Player
{
public:
Archer() { INIT_TL(Archer) }
};
실제 예제 코드는 다음과 같다 :
Player* player = new Knight();
bool canCast = CanCast<Knight*>(player);
Knight* knight = TypeCast<Knight*>(player);
delete player;
아참, shared_ptr 도 TypeCast 를 할 수 있다. 아래와 같이 shared_ptr 를 위한 템플릿 함수를 추가하면 된다! 😮
template<typename To, typename From>
shared_ptr<To> TypeCast(shared_ptr<From> ptr)
{
if (ptr == nullptr)
return nullptr;
using TL = typename From::TL;
if (TypeConversion<TL>::CanConvert(ptr->_typeId, IndexOf<TL, remove_pointer_t<To>>::value))
return static_pointer_cast<To>(ptr);
return nullptr;
}
template<typename To, typename From>
bool CanCast(shared_ptr<From> ptr)
{
if (ptr == nullptr)
return false;
using TL = typename From::TL;
return TypeConversion<TL>::CanConvert(ptr->_typeId, IndexOf<TL, remove_pointer_t<To>>::value);
}
shared_pointer 변환에는 static_cast 가 아닌, static_pointer_cast 를 사용함에 유의하자(C++ Native 에서는). 😀
예제는 비슷하게 나온다 🤣
shared_ptr<Player> player = MakeShared<Knight>();
shared_ptr<Archer> archer = TypeCast<Archer>(player);
bool canCast = CanCast<Mage>(player);
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 3-2. 소켓 프로그래밍 기초 #2 (0) | 2023.09.20 |
---|---|
[C++ 게임 서버] 3-1. 소켓 프로그래밍 기초 #1 (0) | 2023.09.19 |
[C++ 게임 서버] 2-9. Object Pool (0) | 2023.09.15 |
[C++ 게임 서버] 2-8. 메모리 풀 #3 (0) | 2023.09.14 |
[C++ 게임 서버] 2-7. 메모리 풀 #2 (0) | 2023.09.13 |