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

클래스 정보 생성하기
C++Reflection
avatar
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;
	}
}

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

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

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

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

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

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

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

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

using ClassType = MyClass;

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

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 이라는 타입을 가지고 있는지 확인하는 역할을 한다. 그리고 SuperClassTypevoid 가 아니라면 true, void 라면 false 가 된다. 두 조건식을 모두 만족해야 true 가 반환된다.

구현 결과

3013

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







- 컬렉션 아티클