
직접 부딪히고 공부하며 작성한 코드라 많이 부족한 예제와 설명일 수 있습니다. 틀린 설명이 있거나 더 좋은 방법이 있다면 의견은 언제나 환영입니다!
이제 본격적으로 리플렉션을 구현해보자. 먼저 클래스의 메타데이터를 담을 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
의 클래스 정보를 조사식으로 찍어 본 결과이다. 각자의 클래스 이름을 가지고 있고, 부모 클래스의 클래스 정보를 정확히 잘 들고있는 것을 볼 수 있다.