• Feed
  • Explore
  • Ranking
/
/
    C++

    [C++] 리플렉션(Reflection) 구현하기 #2

    클래스 정보 생성하기
    C++Reflection
    뇽
    뇽
    2025.01.30
    ·
    9 min read

    3010

    직접 부딪히고 공부하며 작성한 코드라 많이 부족한 예제와 설명일 수 있습니다. 틀린 설명이 있거나 더 좋은 방법이 있다면 의견은 언제나 환영입니다!

    이제 본격적으로 리플렉션을 구현해보자. 먼저 클래스의 메타데이터를 담을 ClassInfo 를 만들어볼 텐데, 여기서 목표는 다음과 같다.

    1. 클래스 이름을 저장 -> 객체가 자신이 어떤 클래스인지 알 수 있어야 한다.

    2. 부모 클래스의 정보 포함 -> 상속 관계를 파악할 수 있도록 한다.

    3. 멤버 변수와 함수 관리 -> 클래스 내부의 변수와 함수 목록을 저장한다.

    4. 런타임 접근 가능 -> 저장된 변수와 함수를 런타임에 접근할 수 있도록 한다.

    ClassInfo 를 생성하는 매크로는 아래와 같다.

    #define GENERATED_BODY(ClassName) \
    public: \
    	using SuperClassType = typename SuperClassTypeDeduction<ClassName>::Type; \
    	using ClassType = ClassName; \
    \
    	static ClassInfo& GetClass()  \
    	{ \
    		static ClassInfo classInfo{ ClassInfoInitializer<ClassType>( #ClassName ) }; 
    		return classInfo; \
    	} \
    private : \
    \

    ClassInfo 클래스는 다음과 같은 함수와 변수를 가지고 있다.

    class ClassInfo
    {
    public:
    	// 클래스 정보를 초기화하는 생성자
    	template<typename T>
    	ClassInfo(const ClassInfoInitializer<T>& initializer)
    		: className(initializer.name)
    		, superClassInfo(initializer.superClassInfo)
    	{
    	}
    
    	// 변수와 함수를 추가해주는 함수
    	void AddProperty(std::string name, Property* property);
    	void AddFunction(std::string name, Function* function);
        
        // 변수와 함수의 이름을 받아 반환해주는 함수
    	Property* FindPropertyByName(std::string name);
    	Function* FindFunctionByName(std::string name);
        
        // 자신의 정보를 출력하는 함수
    	void PrintClassInfo();
    
    private:
    	std::string className; // 클래스 이름
    	const ClassInfo* superClassInfo; // 부모 클래스 정보
    	std::unordered_map<std::string, Property*> properties; // 변수 목록
    	std::unordered_map<std::string, Function*> functions; // 함수 목록
    };
    3012

    리플렉션이 완료된 후, PrintClassInfo() 함수를 호출해 클래스 정보를 호출한 결과물이다. 오늘 포스팅에서는 화살표로 표시해둔 부분을 출력하는 것을 목표로 한다고 보면 된다!

    class SuperClass
    {
    	GENERATED_BODY(SuperClass)
    };
    
    class MyClass : public SuperClass
    {
        GENERATED_BODY(MyClass)
    };

    나는 클래스 내부에 GENERATED_BODY(클래스명) 매크로를 넣어 클래스 정보를 자동으로 등록하는 방식을 선택했다.

    class MyClass : public SuperClass
    {
    public:
    	using SuperClassType = typename SuperClassTypeDeduction<MyClass>::Type;
        using ClassType = MyClass;
        
        static ClassInfo& GetClass()
        {
            static ClassInfo classInfo{ ClassInfoInitializer<ClassType>("MyClass") };
            return classInfo;
    	}
    }

    MyClass 의 GENERATED_BODY 매크로를 확장해보면 다음과 같다.

    1. using SuperClassType = typename SuperClassTypeDeduction<MyClass>::Type;
      using ClassType = MyClass; : 부모 클래스 타입과 자신의 타입을 별칭으로 선언한다. 클래스에서 동일한 방식으로 자기 자신의 타입과 부모 타입을 참조할 수 있도록 하기 위함이다.

    2. static ClassInfo& GetClass() : 클래스 정보를 가져오는 역할을 하는데, 인스턴스 없이 호출해야 하므로 static을 붙혔다.

    3. static ClassInfo classInfo{ ClassInfoInitializer<ClassType>("MyClass") }; : 모든 인스턴스에서 공유되는 단 하나의 ClassInfo 객체를 만들어 주었다. 객체를 생성할 때 ClassInfoInitializer 를 사용해 클래스의 이름과 부모 클래스 정보를 초기화한다.

    4. return classInfo; : 여러 번 호출해도 동일한 ClassInfo 객체를 반환한다. 클래스 정보가 중복 생성되는 걸 방지하고, 언제든 GetClass() 를 호출하면 해당 클래스의 정보를 가져올 수 있다.

    부모 클래스 추론

    using SuperClassType = typename SuperClassTypeDeduction<MyClass>::Type;

    기본 구조체와 부분 특수화 버전 두가지가 있는 것을 알 수 있다.

    기본 구조체의 경우 T 와 U 두 개의 템플릿 매개변수를 받는다. 기본적으로 Type 을 void 로 정의하는데, 아무런 조건도 만족하지 않으면 void 를 반환한다고 보면 된다.

    부분 특수화 버전의 경우, T 내부에 ClassType 이라는 타입이 존재하면 std::void_t<typename T::ClassType> 는 void 가 되며 기본 템플릿의 두 번째 매개변수 U 가 void 로 대체되어 부분 특수화 버전이 선택된다는 것을 알 수 있다. 결과적으로 ClassType 이 존재하면, Type 은 T::ClassType 이 된다.

    void_t 는 특정 조건이 충족되는지 여부를 검사하기 위해 설계된 템플릿 메타프로그래밍 도구라고 한다. SFINAE(템플릿 인자 치환에 실패해도 컴파일 에러가 발생하지 않고, 단순히 후보에서 제외된다) 원리를 활용해, 컴파일 타임에 타입이나 표현식의 유효성을 확인하는 데 사용된다.

    1. std::void_t<T::ClassType> 이 유효한 타입이면 -> void 로 치환되어 부분 특수화 버전이 선택됨

    2. T::ClassType 이 존재하지 않으면 -> 치환 실패(SFINAE) -> 기본 템플릿 사용

    다시 정리하면, SuperClass 는 SuperClassTypeDeduction 이 실행될 때 ClassType이 존재하지 않으므로 SuperClassType 이 void 가 되는 것이고, MyClass 는 ClassType 이 이미 있으므로(SuperClass 에서 정의됨) SuperClassType 이 SuperClass 가 되는 것이다.

    using ClassType = MyClass;

    부모 클래스 추론이 완료되면, 자신의 ClassType 을 MyClass 로 정의한다.

    ClassInfo 객체 생성

    ClassInfo()

    template<typename T>
    ClassInfo(const ClassInfoInitializer<T>& initializer)
    	: className(initializer.name)
    	, superClassInfo(initializer.superClassInfo)
    {
    }

    ClassInfo 의 생성자에서는 단순히 초기화 구조체를 받아 className 에 이름을 설정하고, superClassInfo 에 부모 클래스의 정보를 넣어준다.

    ClassInfoInitializer

    template <typename T>
    struct ClassInfoInitializer
    {
    	ClassInfoInitializer(std::string name)
    		: name(name)
    	{
    		if constexpr (HasSuper<T>)
    		{
    			superClassInfo = &T::SuperClassType::GetClass();
    		}
    	}
    
    	std::string name;
    	const class ClassInfo* superClassInfo = nullptr;
    };

    클래스 정보 초기화 구조체에서는 인자로 클래스의 이름을 받아 이름을 설정하고, 부모 클래스가 있는지의 여부를 확인해서 부모 클래스의 클래스 정보를 받아오는 역할을 한다. 여기서 if constexpr 을 사용하는데, 컴파일 타임에 조건을 검사했을 때 false 가 나오면 해당 코드 블록 자체가 제거된다.

    template <typename T>
    concept HasSuper = requires { typename T::SuperClassType; }
    && !std::same_as<typename T::SuperClassType, void>;

    HasSuper<T> 는 concept 로, 템플릿의 제약 조건을 정의하는 기능이다. T 클래스가 SuperClassType 이라는 타입을 가지고 있는지 확인하는 역할을 한다. 그리고 SuperClassType 이 void 가 아니라면 true, void 라면 false 가 된다. 두 조건식을 모두 만족해야 true 가 반환된다.

    구현 결과

    3013

    GetClass() 를 통해 SuperClass 와 MyClass 의 클래스 정보를 조사식으로 찍어 본 결과이다. 각자의 클래스 이름을 가지고 있고, 부모 클래스의 클래스 정보를 정확히 잘 들고있는 것을 볼 수 있다.







    - 컬렉션 아티클