
직접 부딪히고 공부하며 작성한 코드라 많이 부족한 예제와 설명일 수 있습니다. 틀린 설명이 있거나 더 좋은 방법이 있다면 의견은 언제나 환영입니다!
이제 본격적으로 리플렉션을 구현해보자. 먼저 클래스의 메타데이터를 담을 ClassInfo 를 만들어볼 텐데, 여기서 목표는 다음과 같다.
클래스 이름을 저장 -> 객체가 자신이 어떤 클래스인지 알 수 있어야 한다.
부모 클래스의 정보 포함 -> 상속 관계를 파악할 수 있도록 한다.
멤버 변수와 함수 관리 -> 클래스 내부의 변수와 함수 목록을 저장한다.
런타임 접근 가능 -> 저장된 변수와 함수를 런타임에 접근할 수 있도록 한다.
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; // 함수 목록
};
리플렉션이 완료된 후, 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 매크로를 확장해보면 다음과 같다.
using SuperClassType = typename SuperClassTypeDeduction<MyClass>::Type;using ClassType = MyClass;: 부모 클래스 타입과 자신의 타입을 별칭으로 선언한다. 클래스에서 동일한 방식으로 자기 자신의 타입과 부모 타입을 참조할 수 있도록 하기 위함이다.static ClassInfo& GetClass(): 클래스 정보를 가져오는 역할을 하는데, 인스턴스 없이 호출해야 하므로static을 붙혔다.static ClassInfo classInfo{ ClassInfoInitializer<ClassType>("MyClass") };: 모든 인스턴스에서 공유되는 단 하나의ClassInfo객체를 만들어 주었다. 객체를 생성할 때ClassInfoInitializer를 사용해 클래스의 이름과 부모 클래스 정보를 초기화한다.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(템플릿 인자 치환에 실패해도 컴파일 에러가 발생하지 않고, 단순히 후보에서 제외된다) 원리를 활용해, 컴파일 타임에 타입이나 표현식의 유효성을 확인하는 데 사용된다.
std::void_t<T::ClassType>이 유효한 타입이면 ->void로 치환되어 부분 특수화 버전이 선택됨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 가 반환된다.
구현 결과

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