개인 프로젝트에서 GameInstanceSubSystem들로 매니저 클래스를 만들면서 원래 인스턴스를 받아오기 위해 매 클래스마다 GetInstance라는 정적 함수를 정의해 간단하게 인스턴스를 불러왔었다
그런데 매니저 클래스가 많아지면서 매 클래스마다 똑같은 내용의 함수를 정의해주는게 불필요하다고 느껴져 다른 방법을 찾아본 결과 모든 매니저 클래스의 부모 클래스를 정의해 여기에 함수를 정의하고 자식 클래스들이 이를 사용하게 만들면 어떨까라는 생각이 들었다
그런데 한 가지 문제점이 부모 클래스에서는 자식 클래스를 모르기 때문에 이를 호출하는 곳에서 부모 인스턴스를 먼저 받아온 후 캐스팅을 해줘야 한다. 이렇게 되면 불필요한 캐스트 절차가 필요해진다
그래서 생각해본 결과 정적 함수를 템플릿화 시키기로 했다. 그러면 호출하는 곳에서는 다음과 같이 호출해 인스턴스를 받아온다
template<typename T>
static T* GetInstance(UObject* WorldContextObject)
{
if (WorldContextObject == nullptr) return nullptr;
UGameInstance* GameInstance = WorldContextObject->GetGameInstance();
if (GameInstance)
{
return GameInstance->GetSubsystem<T>();
}
return nullptr;
}
ULKAccountManager* AM = ULKAccountManager::GetInstance<ULKAccountManager>(GetWorld())
뭔가 이상하다고 생각이 들면 정답이다. ULKAccountManager의 네임스페이스에서 GetInstance를 호출할 때 <T>에 다른 매니저 클래스를 넣으면 그 클래스의 인스턴스가 불러와진다
내가 필요한건 ULKAccountManager의 인스턴스였는데 이러면 코드 작성 의도가 사라진다
그렇게 방법을 찾아본 결과? CRTP(Curiously Recurring Template Pattern)라는 패턴을 알게 되었고 직접 사용해본 결과 내가 원하던 행동을 정의할 수 있었다
그래서 이 패턴에 대해 알아보자
해석해보면 "이상하게 재귀하는 템플릿 패턴"이다. 말이 이상할 수 있는데 실제 사용 예시를 보면 이해할 수 있다
template<typename Derived>
class Base
{
public:
void Interface()
{
static_cast<Derived*>(this)->Implementation();
}
};
// 자식 클래스
class Child : public Base<Child>
{
public:
void Implementation();
};
Base라는 부모 클래스가 있는데 템플릿 클래스로 되어있다. 그리고 Child 클래스는 Base 클래스를 상속받는데 자기 자신을 Base에게 전달하고 있다
이렇게 부모 클래스를 상속받았는데 다시 부모 클래스에게 자기 자신을 넘겨주는 것을 보고 저런 명칭이 붙은 것 같다
이제 이게 왜 필요하나 싶을텐데 이거랑 똑같은 코드를 한 번 보자
class Base {
public:
virtual void Implementation() = 0;
};
class Child : public Base {
public:
virtual void Implementation() override { ... }
};
바로 자식 클래스가 부모 클래스의 함수를 오버라이드한 결과와 똑같다
그냥 우리가 많이 알고 잘 사용하는 밑에꺼 쓰면 안돼?라고 할 수 있겠는데 뭐가 다른지는 C++ 개발자라면 알고 있어야 되지 않을까??
바로 가상 함수 호출은 가상 함수 테이블 등등 추가적인 오버헤드가 드니까 이런 패턴이 등장한 것 같다
좀 전문적으로 정의해보면
CRTP는 동적 다형성, 즉 부모 클래스 포인터에 자식 클래스 객체의 포인터를 대입해 사용하는 것을 피하며 가상 함수의 오버라이드를 흉내내는 템플릿 기법이다
한 문장으로, 가상 함수 호출 비용을 없앨 수 있다
실제 예시는 다음 블로그에서 확인할 수 있으니 한 번 필요성을 느껴보자