Coding Memo

Type Cast 본문

Game Server (C++)

Type Cast

minttea25 2022. 10. 11. 20:53

본 포스팅은 인프런에 등록되어 있는 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