상세 컨텐츠

본문 제목

TypeCast

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 10. 29. 23:11

본문

TypeCast는 Modern C++ Design책에 나오는 내용이다. 필수 알아야 할 기능은 아니지만 해당 내용을 공부하면서 얻게 되는 지식들이 꽤 쓸만하다.

 

class Player
{
public:
    virtual ~Player(){}

}

class Knight : public Player
{
public:

}

class Mage : public Player
{
public:

}

int main()
{
    Player* p1 = new Knight();
    Player* p2 = new Mage();

    Knight* k1 = static_cast<Knight*>(p2);
}

Mage pointer인 p2를 Knight pointer로 캐스팅하면 당시 오류는 발생하지 않지만 실제로 메모리 참조가 잘못되고 있다. dynamic_cast를 사용하면 잘못 캐스팅되는 부분을 잡을 수 있지만, 속도가 느려서 잘 사용하지 않는다.

 

템플릿을 이용하여 이런 문제를 해결해보자.

TypeCast.h

#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

위 코드를 이해하기 위해 다음 코드를 살펴보자.

template<typename T, typename U>
struct TypeList
{
    using Head = T;
    using Tail = U;
}

int main()
{
    TypeList<Mage, Knight>:: whoAmI;
    TypeList<Mage, Knight>:: whoAmI2;
    TypeList<Mage, TypeList<Knight, Archer>::Head whoAmI3;
    TypeList<Mage, TypeList<Knight, Archer>::Tail::Head whoAmI4;
    TypeList<Mage, TypeList<Knight, Archer>::Tail::Tail whoAmI5;
}

TypeList<Mage, TypeList<Knight, Archer>::Tail::Head whoAmI4; 여기서 whoAmI4 는 Knight 타입이다.

이를 일반화하여 만든 것이 template TypeList이다. 3번째 정의를 보면 위 형식과 같다는 것을 알 수 있다.

 

TypeList 개수를 알기 위한 Length를 정의하자.

TypeCast.h

...
#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 main()
{
    int32 len1 = Length<TypeList<Mage, Knight>>::value;
    int32 len2 = Length<TypeList<Mage, Knight, Archer>>::value;
}

enum 값은 컴파일 타임에 결정된다. 따라서 Length template value값은 컴파일 타임에 결정된다.

 

TypeList에서 원하는 index의 Type을 가져오는 기능을 만들어보자.

TypeCast.h

...
#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

원하는 index가 0이 아니면 Head를 없애고 TypeAt을 재귀적으로 호출한다.

int main()
{
    using TL = TypeList<Mage, Knight, Archer>;
    TypeAt<TL, 0>::Result whoAmI6;
    TypeAt<TL, 1>::Result whoAmI7;
    TypeAt<TL, 2>::Result whoAmI8;
}

 

위와 반대로 타입의 index를 찾는 기능을 만들어 보자.

TypeCast.h

...
#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
int main()
{
    int32 index1 = IndexOf<TL, Mage>::value;
    int32 index2 = IndexOf<TL, Archer>::value;
    int32 index3 = IndexOf<TL, Dog>::value;
}

Type from에서 Type to로 변환이 가능한지 알아보는 기능을 만들어 보자.

TypeCast.h

...
#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

From이 To로 변환이 되면 static Small Test(const To&)를 사용하고 그렇지 않으면 static Big Test(...)을 사용하게 된다.

int main()
{
    bool canConvert1 = Conversion<Player, Knight>::exists;
    bool canConvert2 = Conversion<Knight, Player>::exists;
    bool canConvert3 = Conversion<Knight, Dog>::exists;
}

 

다음으로 C# as, is와 같은 기능을 하는 템플릿 함수를 만들어 보자.

먼저 주어진 타입 리스트의 모든 type cast 가능 여부를 저장하는 테이블을 만든다.

...
#pragma region TypeCast

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<int32 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];
...

 

함수 MakeTable에서 하고 싶은 동작은 runtime 일반 함수로 표현하면 다음과 같다.

TypeConversion()
{
    for (int i=0; i<length; i++)
    {
        for (int j=0; j<length; 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;
        }
    }
}

public:
    static bool s_convert[length][length];

i와 j는 런타임에 결정되므로 template에서 사용 불가능하다. 따라서 Int2Type struct를 이용한 것이다.

 

만든 type cast 테이블을 활용하는 템플릿 함수를 만들어 보자.

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>
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(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);
}

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
...

remove_pointer_t<T>는 T가 포인터이면 포인터를 떼어버린 타입을 반환한다.

static_pointer_cast<T>는 스마트 포인터 형변환 때 사용한다.

 

지금까지 만든 기능을 사용하기 위해 2개의 선언을 만든다.

...
#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
{
	Mage() { INIT_TL(Mage); }
};

class Archer : public Player
{
	Archer() { INIT_TL(Archer); }
};

int main()
{
	{
		Player* player = new Knight();

		bool canCast = CanCast<Knight*>(player);
		Knight* knight = TypeCast<Knight*>(player);

		delete player;
	}

	{
		shared_ptr<Knight> knight = MakeShared<Knight>();

		shared_ptr<Player> player = TypeCast<Player>(knight);
		bool canCast = CanCast<Player>(knight);
	}
}

 

1. TypeList 어떻게 동작하는지?

- 첫 번째 부분 : template 부분에 어떠한 타입도 들어갈 수 있다는 의미.

  첫 번째 부분은 동작하지 않음. 템플릿 특수화 사용하기 위해 정의한 것.

- 두 번째 부분 : template 부분에 2 개의 타입이 들어왔을 때 동작.

- 세 번째 부분 : template 부분에 3 개 이상의 타입이 들어왔을 때 동작.

2. Length 어떻게 동작하는지?

- 첫 번째 부분 : 템플릿 특수화 이용 위함

- 두 번째 부분 : TypeList 가 비어있으면 0으로 값 세팅

- 세 번째 부분 : 두 번째 부분 갈 때까지 1씩 증가 시키면서 lengh 계산.

3. TypeAt 어떻게 동작하는지?

- 1 처럼 Tail, Head 를 사용하여 접근하는 부분을 개선하기 위함.

- 2 처럼 세 번째 부분이 일반적인 부분, 두 번째 부분이 마지막 부분임.

- 두 번째 부분 : 세 번째 부분에서 index 가 0 일 때, Head를 Result로 세팅.

- 세 번째 부분 : Type List에서 Head를 하나씩 빼고, index도 1 씩 줄이면서, Result 계산.

4. IndexOf 어떻게 동작하는지?

- 두 번째 부분 : 찾으려는 타입이 Head인 경우 0 으로 값 세팅.

- 세 번째 부분 : 찾으려는 타입 없으면 -1 으로 값 세팅.

- 네 번째 부분 : Head를 하나씩 제거하면서 값은 1 늘리면서, 찾으려는 타입 찾으면 해당 값으로 세팅.

5. Conversion 어떻게 동작하는지?

- MakeFrom 리턴 값 From이 To로 변환 되면 Test가 Small을 반환, 그렇지 않으면 Big을 반환하는 로직을 이용함.

6. TypeConversion 에서 MakeTable이 런타임 변수 i, j를 해결하기 위해 사용한 방법은?

- template 구조체 IntToType을 정의하여 컴파일에 i, j마다 각각 다른 구조체를 만들게 함.

- IntToType<T> 에서 T를 length 로 받는 특수 template 함수 MakeTable 을 만들어 재귀 탈출함.

출처 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

소켓 프로그래밍 기초 #2  (0) 2021.11.04
소켓 프로그래밍 기초 #1  (0) 2021.11.03
Object pool  (0) 2021.10.01
Memory pool #3  (0) 2021.09.30
Memory pool #2  (0) 2021.09.23

관련글 더보기

댓글 영역