Coding Memo
템플릿 특수화, constexpr if 그리고 concept 본문
먼저 간단하게 각각 짧게 요약해보자.
템플릿 특수화 (Template Specialization): 템플릿에서 특정 타입이나, 조건에 맞게 별도로 구현해 놓는 것.
if constexpr: if 조건문의 constexpr 버전으로, 컴파일 타임에 해당 조건을 확인하고 false인 경우 해당 블록을 컴파일 하지 않는다.
concept: C++20부터 추가된 기능으로, 템플릿에 조건을 추가하여 특정 타입이 조건에 맞는지 검증할 수 있는 기능.
이번 글은 템플릿 특수화 대신 if constexpr을 사용하는 것과, concept 타입에 대해서도 템플릿 특수화를 사용해 보는 것을 포함한다. 또한 템플릿 특수화와 if constexpr을 사용할 상황에 대한 내 생각도 작성해보겠다.
(이 글을 기록하는 이유는 concept를 사용한 템플릿 함수에서도 특수화가 제대로 적용될까하는 생각이 들었고 (뭐, 당연하겠지만), 무엇보다도 프로젝트에서의 분리된 여러파일에 대해서 템플릿 특수화를 사용하지 못하는 경우가 있었기 때문이다.)
예시로 GameObject와 Components 클래스를 작성해보겠다.
코드를 간단히 설명하면,
GameObject는 여러 컴포넌트들을 가질 수 있다. GetComponent를 통해서 컴포넌트를 가져올 수 있고, AddComponent를 토애서 컴포넌트를 추가할 수 있다.
concept를 활용하기 위해 Component를 상속하는 클래스와 단순 new 생성을 위해 기본 생성자가 포함되어 있는 클래스를 _COMPONENT로 두었다. 그리고 이 _COMPONENT로 AddComponent와 GetComponent 템플릿 메서드를 생성했다.
class Component {};
class Transform : public Component {};
class Behaviour : public Component {};
class Renderer : public Component {};
template<class T>
concept _COMPONENT = requires
{
requires std::derived_from<T, Component>;
requires std::constructible_from<T>; // 간단하게...
};
class GameObject
{
public:
explicit GameObject() : _transform(new Transform()) {}
~GameObject() { delete _transform; }
Transform* transform() { return _transform; }
template<_COMPONENT Com>
void AddComponent()
{
auto idx = std::type_index(typeid(Com));
auto it = _components.find(idx);
if (it == _components.end())
{
Com* com = new Com();
_components.insert(std::make_pair(idx, com));
}
}
template<_COMPONENT Com>
Com* GetComponent()
{
auto it = _components.find(std::type_index(typeid(Com)));
if (it != _components.end())
{
Com* com = static_cast<Com*>(it->second);
if (com) return com;
else return nullptr; // crash
}
else return nullptr;
}
private:
std::unordered_map<std::type_index, Component*> _components;
Transform* _transform;
};
고려해야 하는 상황은 이러하다.
GameObject는 기본적으로 Transform이란 클래스를 가지고 있어야 한다. 따라서 _components에 속하지 않는 Transform을 _transform 멤버로 가지고 있다. 사용자가 GameObject에서 Transform을 얻고 싶다면, 단순히 transform() 메서드를 통해 가져오면 된다.
그러나, AddComponent<Transform>을 통해 Transform을 추가하려는 사용자 코드가 생긴다면 어떨까. _components에 새로운 Transform이 생성이 되어 버린다.
마찬가지로, 사용자 코드에서 GetComponent<Transform>을 호출 했다면, GameObject의 진정한 Transform이 아닌, 이전에 생성된 Transform이 반환이 되거나, 이마저도 없을 경우, nullptr이 반환이 될 것이다.
실제 GameObject의 위치 정보를 가지고 있는 것은 _transform인데, 엉뚱한 Transform을 활용하게 되는 코드가 생길 수도 있다는 말이다.
우리는 이 상황을 방지하고 싶다.
먼저 간단한 방법을 생각해보자.
1. 사용자가 AddComponent<Transform>과 GetComponent<Transform>을 사용하지 못하게 막는다.
-> 코드의 일관성이 떻어질 수도 있을 것 같고 어떻게 막을건데?! 'API 문서에 사용하지 마세요' 라고 하기는 별로다.
2. Transform 클래스를 Component에서 제외한다.
-> Transform이 Component를 반드시 상속해야하는 상황일 수 있다.
3. Transform을 제외한 다른 컴포넌트 클래스들에 대해서 추가적인 추상화를 더한다.
-> 예를 들어, ExternalComponent : public Component를 추가 하고 Transform 이외의 다른 컴포넌트들이 이 클래스를 상속하면 될 것이다. 위 2가지 방법 보다는 나은 방안이다만, 계층 구조가 더 불편해질 것이다.
너무 이것저것 적어놓았는데, 이를 해결할 수 있는 가장 간단한 방법은 AddComponent와 GetComponent에 대해 템플릿 특수화를 하는 것이다. 명시적 특수화 (Explicit Specialization)을 통해 이 두 템플릿 함수가 Transform 타입에 대해서만 다른 동작을 하도록 할 수 있다.
template<>
void AddComponent<Transform>()
{
return;
}
template<>
Transform* GetComponent()
{
return _transform;
}
만약 사용자 코드에서 AddComponent<Transform>을 호출 했을 때, 단순히 return을 시키고,
GetComponent<Transfom>을 호출 했을 때는 이미 존재하는 _transform을 반환한다.
concept에 대해서도 템플릿 특수화를 동일하게 사용할 수 있다!
정말 쉽게 해결 되었다!!
...
한가지 더 짚고 넘어가보자.
템플릿 특수화는 특수화하려는 타입이 complete해야 한다.
즉, 기본 메타 정보(헤더)가 필요하다는 말이다. 위 코드에서도 GameObject 클래스 정의 이전에 같은 파일에서 Transform에 대한 클래스가 정의되어 있었기 때문에 가능했던 것이다.
그렇다면, Transform과 GameObject가 각각 다른 헤더 파일에 정의되어 있다면...?
물론, GameObject 보다 Transform 클래스가 컴파일 순서상 먼저 컴파일이 될 수 있어서, GameObject 헤더에 '#include "Transform.h"'를 추가한다면 전혀 상관 없을 것이다.
만약 그렇지 않다면?!
전방 선언을 통해 템플릿 특수화를 구현할 수 있을까? 구현할 수 없다.
위에서도 언급했지만, 템플릿 특수화에는 단순 포인터참조가 아닌, 해당 타입에 대한 정보가 필요하기 때문이다.
실제로 아래와 같은 에러가 나타난다.
이 때 사용할 수 있는 것이, 'constexpr if' 이다.
이름에 constexpr이 들어가있는 것처럼, 이 if문은 컴파일 타임에 조건이 평가되는 구문이다.
컴파일 타임에 조건이 true이면 해당 블럭에 있는 코드를 컴파일하고, false라면 해당 블럭을 완전히 무시하고 컴파일에서도 제외시켜버린다.
좀 더 자세하게 말하자면,
C++에서는 템플릿 객체에 대해 컴파일 타임에 인스턴스가 생성이 되는데,
마찬가지로 constexpr if도 컴파일 타임에 평가가 되므로,
템플릿 특수화 대신에 constexpr if를 통해 구현 내부에서 다른 동작을 취하도록 컴파일 타임에 코드를 생성 할 수 있다는 뜻이다.
template<_COMPONENT Com>
void AddComponent()
{
if constexpr (std::is_same_v<Com, Transform>)
{
return;
}
auto idx = std::type_index(typeid(Com));
auto it = _components.find(idx);
if (it == _components.end())
{
Com* com = new Com();
_components.insert(std::make_pair(idx, com));
}
}
template<_COMPONENT Com>
Com* GetComponent()
{
if constexpr (std::is_same_v<Com, Transform>)
{
return _transform;
}
auto it = _components.find(std::type_index(typeid(Com)));
if (it != _components.end())
{
Com* com = static_cast<Com*>(it->second);
if (com) return com;
else return nullptr; // crash
}
else return nullptr;
}
std::is_same_v<>는 constexpr 구조체이기 때문에, constexpr if 조건으로 사용할 수 있다.
들어온 _COMPONENT 타입이 Transform 타입이라면, AddComponent<Transform>과 GetComponent<Transform>에 대한 인스턴스의 코드가 따로 생성 될 것이다.
Transform 타입에 대해서는 아래 코드가 실행이된다.
template<_COMPONENT Com>
void AddComponent()
{
return;
}
template<_COMPONENT Com>
Com* GetComponent()
{
return _transform;
}
다시 말하지만, 결국에는 템플릿 특수화를 했을 때의 코드와 constexpr if를 사용 했을 때의 코드가 동일하다는 것을 알 수 있다.
단지, 템플릿 특수화를 하려는 타입이 incomplete한 상태라면, constexpr if를 통해 컴파일 시점에 특정 타입을 확인하는 코드를 추가할 수 있다.
더 간단하게 말하면, 어떤 경우에도 단순히 constexpr if를 사용해도 된다.
(내 생각에는 if문으로 인해 코드 가독성이 아주 조금 떨어질 수 있는 문제가 생길 수도 있을 것 같다. 위 코드에서는 return 한 줄로 끝났지만, 그 코드가 좀 길어진다면 읽기 상당히 불편하다. 명시적으로 확실하게 하는 좋은 방법은 역시 해당 타입이 함수 시그니처에 정이된 템플릿 특수화인 것 같다.)
'Language > C++' 카테고리의 다른 글
브릿지 패턴 (Bridge Pattern) + Pimpl (0) | 2024.11.26 |
---|---|
[LNK2005] already defined in ~~.obj / inline 함수 (0) | 2024.10.28 |
[C++20] concepts - requires (0) | 2024.10.02 |
Singleton 패턴 (0) | 2024.09.26 |
std::sort와 std::list.sort (1) | 2024.09.26 |