멤버 타입 매크로

김가람·2023년 4월 30일
1

이전 글에서 만든 this_type을 어떻게 써먹을까 고민하다가 자체 게임 엔진 제작을 시작했다주객이 전도된거같지만 그냥 넘어가자. 아무튼 일단 대다수의 게임 엔진이 그렇듯, 가비지 콜렉션을 지원하고 싶었다. 일단 그러면 런타임에 프로그램의 구조를 검사할 수 있어야하니, 리플렉션을 구현해야한다. 또 REFLECT_CLASS(typename .....) 이런 노가다를 안하려면 셀프 타입 앨리어스가 필요하기도 하다. 여하간 그런 목적성을 가지고 리플렉션을 구현하기 시작했다.

필요한 기능

일단 두루뭉실하게 나열해보면 다음과 같다.

  1. 컴파일타임에 정보의 바인딩이 끝났으면 좋겠다.
  2. 가능하면 런타임에서는 아무런 작업이 일어나지않았으면 좋겠다. 일어나야한다면 최소한으로
  3. 상속이 지원되서, 부모의 정보도 볼 수 있으면 좋겠다
  4. 가능하면 부모의 타입정보도 얻어오자.
  5. 간단하게 쓸 수 있으면 좋겠다 가능하면 언리얼같이 UPROPERTY(...), UFUNCTION(...) 만으로 모든게 처리되면 좋겠다.
  6. 런타임 타입 캐스팅이 지원되었으면 좋겠다.
  7. 리플렉트된 함수의 오버로딩이 가능하면 좋겠다.

위의 요구사항을 코드로 적당히 정제해보면 다음 같은 느낌으로 정리할 수 있다. 이제, 이를 기반으로 대략 무얼 해야 할 지 정리해야 한다.

class Sample : public BaseClass
{
public:
    GENERATE_BODY();
public:
    REFLECT_FUNCTION(add);
    int add(int a, int b)
    {
        std::cout << "add " << a << " and " << b << '\n';
        return a + b;
    }
    int add(std::string x, std::string y){...}
public:
    REFLECT_FIELD(int, x);
    REFLECT_FIELD(int, w);
};
class Test : public Sample
{
public:
    GENERATE_BODY();
    REFLECT_FUNCTION(add)
    double add(double x, double y) { return super::add(x, y); }
};
int
main()
{
    auto* ptr   = new_object<Test>(nullptr);
    auto* clazz = Sample::static_class();
    auto func_add = clazz->find_func("add");
    if (func_add != nullptr)
    {
        int x = func_add->invoke<int>(ptr, 1, 2);
    }
    return 0;
}

필요한 요소들

먼저 1번 목표를 만족하기 위해서는, 멤버 함수와 프로퍼티의 고유한 무언가를 컴파일 타임에 알아낼 방법이 있어야한다. 현실에 적당히 빗대서 설명하면, 당신이 누군가를 부르거나 지칭하려면 그 사람의 이름을 알거나 충분한 컨텍스트가 있어야한다. 그것과 마찬가지로, 컴파일러도 그런게 필요하다. 하지만 우리는 컨텍스트를 제공할 수 없으므로 이름을 알아야한다. C++세계에서 클래스 멤버를 지칭하는 고유한 이름은 포인터이니(컴파일 타임에 결정되고 변하지 않으니까) 이를 컴파일 타임에 알아 낼 수 있으면 성공이다.

2번 목표는 1번에서 얻은 정보를 가공하는 방식에 따라서 달라지니, 구현과정에서 생각할 문제이다.

3번과 4번 목표는 1번과정에서 일어난 정보를 자식클래스에 넘겨줄 수 있으면 해결된다.

5번 목표는 위의 수도 코드에서 정의한 인터페이스를 구현 할 수 있으면 해결된다.

6번은 4번 목표를 이루면 자동으로 처리할 수 있다.

7번은 현재 단계에서는 알 수 없다.

마지막으로 목표들과는 무관하게, 컴파일 타임에 얻어낸 정보를 저장할 컨테이너가 필요하다.

이를 요약해서 정리하면 해야 할 일은 다음과 같다.

클래스 멤버의 포인터와 타입 정보, 그리고 자신의 타입 정보를 얻어내어서, 일종의 컨테이너에 넣은 뒤, 이를 대표할 수 있는 타입으로 가공하는 것과 동시에 자식에게도 넘겨 주어야 한다.

즉, 타입 정보의 추출을 할 메타 펑션, 이를 저장할 수 있는 메타 컨테이너는 필수적이다. 그리고 이렇게 생성된 정보를 자식 클래스가 접근할 수 있도록 하는 무언가가 필요하다.

타입 정보 추출

타입 정보 추출은 얼핏 보면 간단해보인다. 그도 그럴것이, 다음과 같이 매크로와 이전 글에서 만든 this_type으로 타입정보 추출하는건 간단하게 할 수 있다.

#define  REFLECT_FIELD(TYPE, NAME)\
	TYPE NAME;\
	using value_type = TYPE;\
	using pointer_type = value_type this_type::*;

하지만 여기서 문제가 하나있다. 대체 어떻게 2개 이상의 프로퍼티를 구분 할 수 있는가? 안된다. 그럼 얘를 조금 수정해야한다.

#define REFLECT_FIELD(TYPE, NAME)
	TYPE NAME;
	struct TYPE##NAME
    {
        using value_type = TYPE;
        using pointer_type = value_type this_type::*;
    }

매크로 토큰을 이용해서 모든 멤버마다 새로운 타입을 만들어 주니, 이제 멤버를 마음대로 선언 할 수 있지만, 이를 어떻게 가져다 쓸 지 방법이 없다. 그럼 템플릿을 도입해보자. 템플릿 인수를 통해서 구분하고 싶은데, 이를 위해서는 타입이든 논 타입이든 고유한 무언가를 만들어서 넘겨야 한다. 모든 멤버마다 고유한 타입을 만들기 위해서 타입을 만드는 건 순환이다. 그럼 논 타입 파라미터가 후보이다. 유력한 후보는 std::string_view이다. 스트링뷰는 모든 멤버가 고유한 이름을 가지므로 이를 통해 어떤 타입을 식별 할 수 있다. 근데 문제가 있다. 타입을 생성하고 식별할 수 있는 키는 맞는데, 첫 값 이후에 다음 값을 알 방법이 없다.

무슨 이야기냐면, 첫 멤버의 이름이 "garam" 이라 치자. 그럼 여기서 내 동생의 이름이 무언지 말할 수 있는가? 수학에서 뭔가 이런 문제를 지칭하는 말이 있을 것 같지만, 아무튼 불가능하다. 그런데, 우리는 이게 가능한 아주 아름답고 단순한 std::size_t라는 친구를 알고 있다. 이 친구에게 있어서 n 다음은 n + 1 이다. 이제 문제는 n + 1을 어떻게 구할 것 인가에 대한 문제로 단순해졌다. 저 값을 직접 넣거나, 혹은 컴파일 타임에 단조적으로 증가하는 컴파일 타임 카운터를 만들어 사용하면 된다. 일단 이 챕터 에서는 이 컴파일 타임 카운터라는 친구가 구현되었다고 가정하고 진행하자. 다시 매크로를 수정해보자.

	template <std::size_t Index>
	struct detail_field_reflection ; // 전방선언

#define REFLECT_FIELD(TYPE, NAME)
	TYPE NAME;
	static constexpr std::size_t detail_##NAME##_field_index = counter<detail_field_reflection>(); // stub
	template <>
	struct detail_field_reflection<detail_##NAME##_field_index>
    {
        using value_type = TYPE;
        using pointer_type = TYPE this_type::*;        
    };

이제 좀 윤곽이 잡혔다. 필드 타입을 대표하는 타입만 세어 주는 카운터로 나의 인덱스를 얻어낸 뒤, 구조체를 특수화 시킨다. 그럼 이제 멤버 프로퍼티의 개수 만큼 재귀를 돌면서 어떤 컨테이너에 넣으면 된다.

일단 타입을 선언하는 매크로를 다음과 같이 고치면 타입을 선언하는 단계는 끝이다!

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

이제 이 매크로가 작동하도록 하기 위한 컴파일 타임 카운터를 구현해야 하는데, 이를 이 글에서 동시에 다루면 너무 글이 길어지므로 다음 글에서 다루도록하겠다.

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

0개의 댓글