Coding Memo
Type Cast 본문
본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.
다음과 같은 클래스가 있다고 가정해보자.
class Player
{
public:
Player() {}
virtual ~Player() {}
};
class Knight : public Player
{
public:
Knight() { }
};
class Mage : public Player
{
public:
Mage() { }
};
class Archer : public Archer
{
public:
Archer() {}
};
상위 클래스인 Player로 관리를 해줄 것인데, 중간중간 관리 리스트에 있는 Knight와 Mage를 사용하고 싶을 때가 있을 것이다.
그럴때, 다음과 같이 캐스팅을 통해 객체를 가져오게 된다.
int main()
{
Player* p1 = new Knight();
Knight* k1 = static_cast<Knight*>(p1); // ???
Knight* k2 = dynamic_cast<Knight*>(p1); // nullptr
}
여기서 문제가 발생할 수 있다. 내부는 Knight로 생성하고 Player로 관리를 하고 있었는데, 이 Player를 Mage로 캐스팅 해버리게 된다면 어떻게 될까?
위 과정에서 dynamic_cast를 사용하였다면 이 캐스팅이 잘못된 캐스팅임을 알아 nullptr을 반환하여 k2가 nullptr로 되어 사용하지 못할 것이다. 여기서 오류를 잡아낼 수 있겠으나, dynamic_cast는 시간과 비용이 꽤 소모되는 방법이다. (캐스팅이 정상적으로 가능한지 확인을 하기 때문이다.)
그러면 static_cast를 사용하면 어떨까. static_cast로 캐스팅 시 런타임 오류가 발생하지 않는다. 해당 캐스팅이 잘못된 캐스팅임에도 불구하고!! (속도는 빠르다.) 후에 이 메모리에 접근하려고 하면 엄청난 문제가 발생할 것이다.
이 문제를 해결하는 방법에는 각 클래스에 type 등과 같은 enum을 사용해서 각자 고유의 타입을 지니게 한뒤, 캐스팅을 하기전에 각 클래스의 내부함수로 캐스팅이 가능한지 확인하는 함수를 작성할 수 있을 것이다.
하지만 클래스가 많아질 수록 복잡해지고 귀찮아지는 작업이 될 수도 있다.
따라서 TypeCast라는 파일을 작성한다.
Type들(class들)을 List 처럼 관리를 하여 클래스간의 캐스팅 가능여부 및 타입 리스트 관리를 하는 파일이다.
이를 컴파일 단계에서 처리하도록 한다! (런타임때 실행되는 것이 아니다.)
(문법이 나중갈수록 좀 복잡해졌다...)
TypeList
TypeList를 생성하는 구조를 만든다. 이때, 리스트의 길이를 알 수 없으므로 typename U가 끝날때까지 struct TypeList<T, U...>를 계속 호출하는 방식으로 한다. (재귀적 호출이라고 생각하면 될 듯 하다.)
#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
테스트 코드
컴파일단계에서 값이 나오기 때문에 직접 실행까지 할 필요는 없다.
Visual Studio에서 마우스 커서만 올려놓아도 whoAMI가 어떤 타입인지 확인이 될 것이다.
TypeList<Mage, Knight, Archer>::Head whoAMI0; // Mage
TypeList<Mage, Knight, Archer>::Tail::Head whoAMI1; // Knight
TypeList<Mage, Knight, Archer>::Tail::Tail whoAMI2; // Archer
Length
TypeList의 길이를 반환해주는 구조이다.
마찬가지로 1개씩 리스트의 내부 타입의 개수를 셀건데, 1+n -> 1 + (1 + (n-1)) -> 1 + (1 + (1 + (n-2)))... 의 방식으로 TypeList 선언과 동일하게 재귀적 호출을 이용한다.
#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
테스트 코드
int len1 = Length<TypeList<Mage, Knight>>::value; // 2
int len2 = Length<TypeList<Mage, Knight, Archer>>::value; // 3
TypeAt
index 값을 받아서 리스트의 해당 index에 어떤 타입이 있는지 반환한다.
#pragma region TypeAt
template<typename TL, int index>
struct TypeAt;
template<typename Head, typename... Tail>
struct TypeAt<TypeList<Head, Tail...>, 0>
{
using Result = Head;
};
template<typename Head, typename... Tail, int index>
struct TypeAt<TypeList<Head, Tail...>, index>
{
using Result = typename TypeAt<TypeList<Tail...>, index - 1>::Result;
};
#pragma endregion
테스트 코드
using TL = TypeList<Mage, Knight, Archer>;
TypeAt<TL, 0>::Result whoAMI3; // Mage
TypeAt<TL, 1>::Result whoAMI4; // Knight
TypeAt<TL, 2>::Result whoAMI5; // Archer
IndexOf
TypeAt과 반대로 클래스(type)으로 index를 가져오는 구조이다.
해당 타입이 없으면 끝까지 재귀호출하여 +1을 하기 때문에 Length+1을 반환한다.
#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
테스트 코드
엉뚱하게 Dog라는 클래스를 넣으면 Length를 벗어난 값을 반환한다.
using TL = TypeList<Mage, Knight, Archer>;
int index1 = IndexOf<TL, Mage>::value; // 0
int index2 = IndexOf<TL, Archer>::value; // 2
int index3 = IndexOf<TL, Dog>::value; // 4 ??
Conversion
상속관계면 메모리 사이즈도 당연히 다를 것이다.
(return 0는 무시)
#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)
};
};
#pragma endregion
테스트 코드
bool canConvert1 = Conversion<Player, Knight>::exists; // Player -> Knight? // false
bool canConvert2 = Conversion<Knight, Player>::exists; // Knight -> Player? // true
bool canConvert3 = Conversion<Knight, Dog>::exists; // Knight -> Dog? // false
TypeConversion
캐스팅 가능한 테이블을 만들고 테이블을 통해 From Type에서 To Type으로 캐스팅이 가능한지 확인한 후, 가능하다면 static_cast를 통해 캐스팅된 값을 반환하고 그렇지 않다면 nullptr을 반환한다.
dynamic_cast를 쓰는것과 비슷하게 볼 수도 있겠다.
참고로 단순 포인터방식을 쓰는 것이 아닌 shared_ptr방식을 프로젝트에서 사용하고 있으므로 shared_ptr로 인자로 받고 shared_ptr을 반환한다. (static_pointer_cast 이용)
Int2Type을 만든 이유는 캐스팅 테이블을 런타임에서가 아니라 컴파일 단계에서 만들기 위함이다.
한가지 주의 할 점은 _typeId를 참조하고 있으므로, TypeList에 들어가는 클래스들은 _typeId를 모두 가지고 있어야한다.
#pragma region TypeCast
// 숫자 하나를 클래스로 인지하도록
template<int v>
struct Int2Type
{
enum { value = v };
};
template<typename TL>
class TypeConversion
{
public:
enum
{
length = Length<TL>::value
};
TypeConversion()
{
MakeTable(Int2Type<0>(), Int2Type<0>());
}
template<int i, int 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<int 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(int from, int 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];
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);
}
else
{
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);
}
#pragma endregion
테스트 코드
using TL2 = TypeList<Player, Mage, Knight, Archer>;
TypeConversion<TL2> test;
test.s_convert[0][0];
사용방법
_typeId를 가지고 있어야하기 때문에 이 부분도 매크로로 만들어주자
#define DECLARE_TL using TL = TL; int _typeId;
#define INIT_TL(Type) _typeId = IndexOf<TL, Type>::value;
마지막으로 클래스에 정의한 매크로를 넣어주자.
상위 객체인 Player에만 TL을 선언해주면 된다.
using TL = TypeList<class Player, class Mage, class Knight, class Archer>;
class Player
{
public:
Player()
{
INIT_TL(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); }
};
진짜 마지막으로 테스트만 하면... (TL은 위에 선언한 방식 그대로)
shared_ptr<Player> player = MakeShared<Knight>();
shared_ptr<Archer> archer = TypeCast<Archer>(player); // nullptr
bool canCast1 = CanCast<Mage>(player); // false
bool canCast2 = CanCast<Archer>(player); // false
bool canCast3 = CanCast<Knight>(player); // true
bool canCast4 = CanCast<Player>(player); // true
이번에도 느낀점은 똑같다. 어떻게 이걸 생각해 냈지........ㅋㅋㅋㅋ.....
마지막 TypeCast 함수도 내부적으로 좀 모르는 부분이 있어서 더 찾아보아야 겠다. (remove_pointer_t ??? 등등...)
'Game Server (C++)' 카테고리의 다른 글
Socket Programming - TCP (0) | 2022.10.31 |
---|---|
Socket Programming Basic (0) | 2022.10.18 |
Object Pool (0) | 2022.10.11 |
Memory Pool (0) | 2022.10.09 |
메모리 할당 - STL Allocator (0) | 2022.10.09 |