컴파일 타임 카운터(인덱스)

김가람·2023년 5월 28일
0

이전 글에서 이어지는 글인데, 아무튼 이제 우리는 컴파일 타임 카운터를 구현해야한다. 이전글에서 썼던 카운터의 형태는 잊어버리도록하자. if exist의 느낌으로 작성했으니 처음부터 컴파일 타임 카운터가 가능한지를 살펴야한다. 구현이 끝난 지금이야 이해가 되지만, 아무튼 여기서 가장 골머리를 앓았었다. 구글에 검색해보면 이전 글에서 언급했던 프렌드 인젝션을 이용한 컴파일 타임 카운터가 돌아다닌다. 하지만 그거 가져다 쓰기는 싫으니 새로 하나 만들어보자.

일단 이전 글에서 구현한 리플렉션용 매크로는 다음과 같다

	template <std::size_t Index>
	struct detail_field_reflection ; // 전방선언
#define REFLECT_FIELD(TYPES, NAME, ...)                                                  
    TYPES NAME{};                                                                        
                                                                                         
    struct detail_##NAME##_field_tag;                                                    
    static constexpr std::size_t detail_##NAME##_field_index = counter<detail_field_reflection>                           
    template <>                                                                          
    struct detail_field_reflection<detail_##NAME##_field_index>                          
    {                                                                                    
        using value_type                       = TYPES;                                  
        static constexpr std::string_view name = #NAME;                                  
        template <class U>                                                               
        using pointer_type = value_type U::*;                                            
        template <class U>                                                               
        static constexpr value_type U::*pointer_value = &U::NAME;                        
    };

아이디어

솔직히 조금 막막했다. 글을 쓰는 지금도 모티브를 어떻게 설명해야 할 지 모르겠다. 일단 결론부터 말하자면, 프렌드 인젝션이 현재 컴파일 상황을 캡쳐해서 써먹듯, 다른 방법을 찾아서 캡쳐를 해보자는 아이디어다. 예를 들자면 인스턴스화 된 detail_field_reflection을 세어보는건 어떤가? 말은 쉽지.

c++20 에는 concept라는 기능이 있다. 대략 다른 글을 인용해서 자세한 설명은 생략하겠다. 템플릿을 인스턴스화 할 때, 어떤 조건을 걸 수가 있는데, 그 조건에는 다음과 같은 것도 포함된다.

template <class T>
concept has_name = requires
{
    typename T::name;
};

타입 T가 어떤 타입을 정의 해야만 인스턴스화를 허가 해주겠다는 그런 뜻인데, 이와 동일하게 타입이 어떤 필드를 가지고 있어야 한다는 것을 조건으로 걸 수 도 있다. 풀어 말하자면, 현재 어떤 템플릿을 인스턴스화 하는데, name이라는 필드를 가지고 있는 타입 T가 존재할 때만 인스턴스화를 시킬 수 도 있는 거다. 말이 좀 복잡한데, 코드로 풀어 말하자면 다음과 같다.

template <typename T> requires has_name<T>
struct counter {};
class Test
{
    template<std::size_t Idx>
	struct View;
    
    static constexpr auto some_name1 = counter<View> // fails since there are no View With Name;
    
    template<>
    struct View<0>
    {
        static constexpr std::string_view name = "A";
    };
    
 	static constexpr auto some_name2 = counter<View> // success since there exist one with name;
};

조금 실마리가 보이기 시작했다. 단순하게 생각해보자. 인스턴스화를 허가할 수 있다면, 이전 글에서 한 것처럼, 특수화를 통해서 어떤 친구가 존재하는지 안하는지 알아 볼 수 있지 않을까? 예를 들자면 다음과 같이 특수화를 컨셉을 사용해 할 수 있다. 그럼 위 코드는 일단은 언제나 컴파일 성공할 수 있다.

template <class T> 
struct counter // 기본형
{
    static constexpr std::size_t value = 0;
};
template <class T> requires has_name<T>
struct counter // 조건이 걸린 특수화
{
    static constexpr std::size_t value = 1;
};

좋다 그러면 얘를 조금 더 발전 시켜보자.

template <class T>
struct counter // name 필드가 없으면 0
{
    static constexpr std::size_t value = 0;
};

template <class T> requires has_name<T>
struct counter // name 필드가 있으면 1을 더하고 다시 name필드가 있는지 체크하기
{
    static constexpr std::size_t value = 1 + counter<T>::value;
};

물론 안된다. 무조건 템플릿 재귀 깊이가 너무 깊어서 에러를 뱉을 것이다. T == View라고 가정하고 View<0>가 name을 가지고 있으면 계속해서 1을 더해가는게 뻔히 보인다. 어떻게 고쳐야 할까? 단순하게 접근하면 된다. View<0>를 체크하는게 아니라 View<1>을 체크하라고 알려주면 된다.

template  <std::size_t Idx, template<std::size_t> class T>
struct counter 
{ 
    ... = 0;
};
template <std::size_t Idx, template <std::size_t> class T> requires has_name<T<Idx>>
struct counter 
{
    ... = 1 + counter<Idx + 1, T>::value;
};

// in use
counter0 = counter<0, View>::vaule;
counter1 = counter<counter0, View>::value;

그런데... 컴파일 여부는 차치하고, 결국은 사람이 직접 인덱스를 써넣는 삽질이 반복된다. 짜증나니 재귀로 처리해버리자

template  <std::size_t Idx, template<std::size_t> class T>
struct counter_impl // 3
{ 
    ... = 0;
};
template <std::size_t Idx, template <std::size_t> class T> requires has_name<T<Idx>>
struct counter_impl // 2
{
    ... = 1 + counter<Idx + 1, T>::value;
};

template <template <std::size_t> class T> 
struct counter // 1
{
    ... = counter_impl<0, T>::value;
};
// in use
// no view that has name
idx0 = counter<View>::value; 
struct View<idx0> {...}; // instantiate one
idx1 = counter<View>::value;

위 코드가 방법론만을 표현한 코드이다. 결국은 처음에는 인덱스 0인 친구에게 Name필드 존재 여부를 체크하는 거니 결국 1, 2, 3 순으로 체크가 이루어질것이다. 근데 컴파일이 안된다... counter_impl<0>는 이미 존재하는데, idx1을 만드는 과정에서 또 인스턴스화를 시키려고 한다.. 결국은 모든 카운터는 유니크해야한다는 결론이다. 이를 해결하기 위해서 태그를 하나 추가하자.

template  <std::size_t Idx, class Tag, template<std::size_t> class T>
struct counter_impl // 3
{ 
    ... = 0;
};
template <std::size_t Idx, class Tag, template <std::size_t> class T> requires has_name<T<Idx>>
struct counter_impl // 2
{
    ... = 1 + counter<Idx + 1, T>::value;
};

template <class Tag, template <std::size_t> class T> 
struct counter // 1
{
    ... = counter_impl<0, T>::value;
};
// in use
// no view that has name
idx0 = counter<struct Tag1, View>::value; 
struct View<idx0> {...}; // instantiate one
idx1 = counter<struct Tag2, View>::value;

근데 이러면 원점이다. 결국은 사람이 직접 유일한 태그를 지정해줘야한다. 이는 우리가 처음 썼던 매크로에서 해결해보자

	template <std::size_t Index>
	struct detail_field_reflection ; // 전방선언
#define REFLECT_FIELD(TYPES, NAME, ...)                                                  
    TYPES NAME{};                                                                        
                                                                                         
    struct detail_##NAME##_field_tag; // 이친구를 태그로 쓰면 된다!!                                                    
    static constexpr std::size_t detail_##NAME##_field_index = counter<detail_##NAME##_field_tag, detail_field_reflection>::value;
    template <>                                                                          
    struct detail_field_reflection<detail_##NAME##_field_index>                          
    {                                                                                    
        using value_type                       = TYPES;                                  
        static constexpr std::string_view name = #NAME;                                  
        template <class U>                                                               
        using pointer_type = value_type U::*;                                            
        template <class U>                                                               
        static constexpr value_type U::*pointer_value = &U::NAME;                        
    };

문제가 깔끔하게 해결됐다! 코드를 쭉 정리 하면 다음과 같다.

namespace refl
{

// compile time index
namespace detail
{
template <std::size_t, class, template <std::size_t> class>
struct index_impl
{
    static constexpr std::size_t value = 0;
};

template <std::size_t I, class Tag, template <std::size_t> class Type>
    requires requires { Type<I>::name; }
struct index_impl<I, Tag, Type>
{
    static constexpr std::size_t value = 1 + index_impl<I + 1, Tag, Type>::value;
};

template <class Tag, template <std::size_t> class Type>
struct index
{
    static constexpr std::size_t value = index_impl<0, Tag, Type>::value;
};

} // namespace detail
namespace refl
#define REFLECT_FIELD(TYPES, NAME, ...)                                                  \
    TYPES NAME{};                                                                        \
                                                                                         \
    struct detail_##NAME##_field_tag;                                                    \
    static constexpr std::size_t detail_##NAME##_field_index =                           \
        refl::detail::index<detail_##NAME##_field_tag, detail_field_reflection>::value;  \
    template <>                                                                          \
    struct detail_field_reflection<detail_##NAME##_field_index>                          \
    {                                                                                    \
        using value_type                       = TYPES;                                  \
        static constexpr std::string_view name = #NAME;                                  \
        template <class U>                                                               \
        using pointer_type = value_type U::*;                                            \
        template <class U>                                                               \
        static constexpr value_type U::*pointer_value = &U::NAME;                        \
    };

class Test
{
public:
    REFLECT_FIELD(int const, View);
};

이제 컴파일 타임에 액세스 가능한 타입 어레이가 생성되었다!! 다음글에서 이 친구들을 어떻게 억세스 할 지 쓰겠다!

profile
C++, 언리얼 개발자입니다. 머신러닝도 조금했었어요.

0개의 댓글