[C++] 템플릿을 사용해서 dynamic_cast 횟수를 줄일 수 없을까요?

개인 공부용으로 간단한 게임을 만들고 있습니다.

게임 내 오브젝트(플레이어, 몬스터 등)들이 보유하게 될 컴포넌트(메쉬, 텍스쳐, 이동, 공격 등)를 컴포넌트 매니저 클래스에서 생성 및 복제를 담당하게 하려 합니다.

원래 구조는 이렇습니다. 오브젝트 생성 시점에서 해당 오브젝트가 필요로 하는 컴포넌트의 이름을 매니저 클래스의 복제함수에 인자로 넣어주면, 해당 키로 맵에서 find를 돌린 뒤, 있으면 해당 컴포넌트를, 없으면 nullptr을 반환하는 식입니다.

물론 매니저 클래스는 초기화 단계에서 모든 종류의 컴포넌트를(일부 제외) 1회 생성해서 키(wstring)와 함께 map에 보관합니다. 후에 오브젝트가 호출하면 해당되는 컴포넌트를 찾아서 복제해주는 것이죠.

이 복제는 각 컴포넌트가 멤버함수로 지니고 있습니다.

CComponent* CTexture::Clone()
{
	return new CTexture(*this);
}

이런 식으로요. 보시다시피 모든 컴포넌트는 CComponent라는 최상위 부모객체를 상속받고 있습니다. 매니저 클래스의 맵도 CComponent* 를 값으로 보관하고 있지요.

여기서 2가지 불편함이 있었습니다.

  1. 모든 반환값이 CComponent라는 최상위 부모객체이므로, 오브젝트가 만약 특정 컴포넌트의 기능이 필요해서 따로 멤버로 보관하고 싶을 경우, 생성시에 dynamic_cast를 통해 컴포넌트 본래의 자료형으로 변환을 해줘야 한다는 점입니다.
// 매니저 클래스에 컴포넌트 Clone을 요청
CComponent*	component = nullptr;
component = buffer_= dynamic_cast<CRectTexture*>(CComponentMgr->Clone(L"Buffer_RcTex"));
componentMap.emplace(L"Buffer", component);

여기서 buffer_는 이 오브젝트가 갖고 있는 멤버변수 입니다. 매 프레임 CRectTexture 객체 안의 render를 호출하기 위해 dynamic_cast로 원래 모습으로 변환해준 뒤 멤버 변수에 담아두고 있습니다.
단, 컴포넌트 맵은 CComponent(부모)형 포인터를 저장하기 때문에, 따로 받아서 키와 함께 넣어준 것입니다.

컴포넌트의 종류가 많아질수록 이렇게 dynamic_cast를 요구하는 것들이 많아지더군요.

  1. 두 번째 불편함은 모든 컴포넌트가 위에 올린 Clone() 함수를 만들어서 갖고 있어야 한다는 점입니다.
CComponent* CComponent::Clone()
{
	return new CComponent(*this);
}

이런 식으로 최상위 클래스에 넣어볼까도 했지만, 보시다시피 new를 할 때 부모형으로 해버려서 제대로 생성이 안 됩니다.

결국 그냥 쓰고 있었는데, 문득 템플릿을 써보면 어떨까 싶더군요. 그래서 이렇게 만들어 봤습니다.

class CComponent
{
// ... 생략...
public:
	template<typename T>
	T*	Clone()
	{
		return new T(*this);
	}
}

최상위 클래스에 이렇게 Clone 함수를 만들고,

// 매니저 클래스에 컴포넌트 Clone을 요청
CComponent*	component = nullptr;
component = buffer_ = componentMgr->Clone<CRectTexture>(BufferID::rectTexture);
componentMap_.emplace(ComponentID::viBuffer, component);

// 입력받은 자료형(<CRectTexture>)의 형태로 Clone해서 반환하는 매니저 클래스
template<typename T>
inline T* CloneComponent(const wstring componentName)
{
	auto componentIteretor = componentMap_.find(componentName);
	if (componentIteretor == componentMap_.end())
		assert(!" 요청한 컴포넌트가 없을 경우 예외처리 ");

	return componentIteretor ->second->Clone()<T>;
}

그리고 빌드를 해보니 이런 에러가 뜹니다.

componentmgr.h(53): error C2672: ‘CComponent::Clone’: 일치하는 오버로드된 함수가 없습니다.
componentmgr.h(53): error C2783: ‘T *CComponent::Clone()’: 'T’의 템플릿 인수를 추론할 수 없습니다.

템플릿을 처음 써보는 거라 뭘 잘못한 건지도 잘 모르겠고, 뭘 검색해봐야 할지도 잘 모르겠네요.

일단 현재 생각중인 해결책(?)은 CComponent에 Clone을 템플릿으로 만드는 걸 포기하고 처음처럼 각 자식 객체마다 따로 만들면 해결되지 않을까 싶은데, 다른 방법은 없는지 궁금합니다.

조언 좀 부탁드립니다.

Clone()< T >가 아니라 Clone< T >()가 아닌가요?


https://en.cppreference.com/w/cpp/language/member_template

멤버 템플릿을 다룬 cppreference 문서입니다

1 Like

얘가 일을 제대로 하고,

얘가 리소스별로 범주화 되면 해결될 문제 같은데요.

2 Likes

프로토타입 패턴을 적용하면 되겠군요.

이 경우 std::variant, std::visitCRTP를 쓰면 모조리 템플릿으로 처리 가능합니다.
단지, 일반적인 virtual 을 이용한 상속보다는 코드가 좀 더 복잡해지죠.

1 Like

대충 예제를 만들어보면 이렇게 되겠네요.

template<class T>
class CComponentInterface
{
// ...
public:
    T* Clone()
    {
        return new T(*static_cast<T*>(this));
    }
// ...
};

class CTexture :
    public CComponentInterface<CTexture>
{
    // ...
}; 

class CMesh :
    public CComponentInterface<CMesh>
{
    // ...
}; 

// ...

using CComponent = std::variant<CTexture, CMesh>;

static std::map<std::string, CComponent*> componentMap{
    { "CTexture", new CTexture() },
    { "CMesh", new CMesh() }
};

template<class …Ts>
struct overload :
    Ts…
{
    using Ts::operator()…;
};

template<class …Ts>
overload(Ts…)–>overload<Ts…>;

overload 구조체는 일종의 함수객체들의 리스트 라고 보시면 됩니다.
맨 아래쪽의 함수 선언은 deduction guide입니다.

사용할 때는 이렇게 하시면 됩니다.

void process(CComponent* component)
{
    std::visit(overload{
        [](CTexture& texture){},
        [](CMesh& mesh){},
        [](auto){}
    }, *component);
}

switch-case문과 비슷하게 생각하시면 됩니다.
맨 아래의 auto를 인자로 받는 함수는 default인 경우와 동일합니다.

5 Likes

헉… 그러고 보니 그러네요;; 바보 같은 실수였군요 ㅜㅜ

감사합니다.

어… 죄송하지만 일을 제대로 한다는 것과 리소스 별로 범주화 된다는 게 무슨 의미인지 잘 모르겠습니다;

CTexture 클래스가 의도한 대로 동작하고, 리소스 종류(텍스쳐, 메쉬 등)에 따라 서로 다른 componentMap을 사용해야 한다는 뜻인가요?

중간 추상클래스들이 활용되어야 함을 말씀드린거죠.
걔네 하부가 렌더 파이프라인에서
공통적으로 가져야 하는 메서드들을 정의하면 되잖아요?
항상 top level 의 CComponent 를 거치니 생기는 문제.

다른말로 하면 러시아 페인트공의 딜레마를 겪고 계신것.

1 Like

@pi40ni33dr
상세한 예제 코드 감사합니다. 템플릿은 처음 써보는데 사용법이 참 다양해서 흥미롭네요.

예제가 아직 완벽히 이해는 안 가는데, 클래스템플릿을 상속받은 객체들이 저렇게 하나로 묶이는(?)게 신기합니다 ㄷㄷ;

제가 사정상 vs2015를 쓰고 있어서 c++17요소들은 현재 프로젝트에 추가할 수가 없지만, 공부해서 다음에 꼭 써먹어 보도록 하겠습니다.

@codesafer
코드가 너무 길어질까봐 생략하긴 했는데, 실제로는 CComponent 를 상속받는 버퍼 클래스가 있고, 본문에 예시로 사용된 CRectTexture 같은 버퍼 클래스들이 이를 상속 받고 있습니다.

(답글 쓰면서 깨달았는데 RectTexture는 버텍스 버퍼 클래스인데 이름이 텍스쳐였네요… 텍스쳐를 입히는 종류라서 그렇게 적은 거였는데 지금 보니 이상하군요 ㅋㅋ)

말씀하신 렌더에 관련된 메서드들이 거기에 정의되어 있고, CComponent에는 원래 아무 것도 없었다가 이번에 모든 하위 컴퍼넌트들이 사용하게 하려는 목적으로 Clone()을 만든 상태입니다.