[C++] 리플렉션(Reflection) 구현하기 #2 - 클래스 타입 정보 생성

연하·3일 전
0

C++

목록 보기
2/2
post-thumbnail

[참고 사이트] https://www.slideshare.net/slideshow/c20-251161090/251161090#32

아무것도 없는 상태에서 리플렉션을 구현하기엔 무리가 있어서, 해당 자료를 참고해 코드를 이해한 뒤 변형하여 쓸 수 있게끔 하는 것을 목표로 하기로 했다. 예제의 부수적인 코드를 모두 제거하고, 클래스 타입 정보 생성만을 위한 내용만 첨부하였다.

class MyClass
{
    GENERATED_BODY(MyClass)
};

GENERATED_BODY 매크로는 크게 두가지 역할을 한다.
1. 클래스에 자신의 타입과 부모 타입을 일정한 이름(별칭)으로 정의
2. 클래스의 타입 정보를 담고 있는 TypeInfo 객체를 생성

자신의 타입과 부모 타입을 별칭으로 정의

// GENERATED_BODY 매크로 코드 중 일부
using Super = typename SuperClassTypeDeduction<TypeName>::Type;
using ThisType = TypeName;

첫번째로 Super라는 부모 클래스 타입의 별칭과 ThisType이라는 자신 타입의 별칭을 선언한다. ThisType은 매크로의 TypeName을 그대로 사용할 수 있지만 부모 클래스의 타입은 부모 클래스가 없는 경우 때문에 추가적인 처리를 해줘야 한다.

매크로 안에서 별칭을 사용하는 이유는, 모든 클래스에서 동일한 방식으로 자기 자신의 타입과 부모 타입을 참조할 수 있도록 강제하기 위함이다.

부모 클래스 추론

이제 부모 클래스를 추론하는 템플릿(SuperClassTypeDeduction)을 작성하자.

// ---------------------------------------
//		Super Class Type Deduction
// ---------------------------------------

template <typename T, typename U = void>
struct SuperClassTypeDeduction
{
	using Type = void;
};

template <typename T>
struct SuperClassTypeDeduction<T, std::void_t<typename T::ThisType>>
{
	using Type = T::ThisType;
};

SuperClassTypeDeduction 은 두 개의 템플릿 매개변수 TU 를 받는다. U 의 기본값으로 void 가 설정되어 있기 때문에, T 가 특정 요구사항을 만족하지 않을 때 Typevoid 로 처리하게 된다.

아래의 특수화된 템플릿은 T 타입이 ThisType 이라는 멤버 타입을 가지고 있는 경우 적용되는 것을 알 수 있다. std::void_t<typename T::ThisType> 를 통해 T::ThisType 이 존재하는지 검사하고, 존재하지 않는다면 특수화는 무시되고 기본 템플릿이 사용된다.

해당 코드는 SFINAE 기법을 사용하는데, 템플릿 인자가 특정 조건을 만족하지 않으면 해당 템플릿을 사용하지 않고 다른 대체 가능한 템플릿을 선택하는 방식을 말한다.

자신의 타입을 나타내는 ThisType 을 부모 타입으로 사용할 수 있는 이유는, 자신의 클래스 내에서 ThisType 을 선언하기 전까지는 부모로부터 상속받은 ThisType 이 사용되기 때문이다.

using Super = typename SuperClassTypeDeduction<TypeName>::Type;
using ThisType = TypeName;

다시 매크로를 확인해보면, SuperClassTypeDeduction 를 통해 만약 부모 클래스가 있다면 Super에 부모 클래스의 타입을 사용하고, 없다면 void 타입을 사용하게 된다는 것을 알 수 있다. 그 후 ThisType 이 자신의 타입을 사용하도록 한다.

typename 키워드를 사용하는 이유는 SuperClassTypeDeduction<TypeName>::Type 이 의존 타입이기 때문이다. 컴파일러는 SuperClassTypeDeduction<TypeName>Type타입인지 값인지를 바로 알 수 없기 때문에, 템플릿 안에서 의존 타입을 명시적으로 표시해야 한다.

TypeInfo 객체 생성

static TypeInfo& StatictypeInfo() 
{ 
	static TypeInfo typeInfo{ TypeInfoInitializer<ThisType>( #TypeName ) }; 
	return typeInfo; 
} 

inline static TypeInfo& m_typeInfo = StaticTypeInfo();

다음 매크로는 클래스에 대한 TypeInfo 객체를 정적 멤버 변수로 선언한다. 정적 멤버 변수로 선언했기 때문에 프로그램 시작 시 생성된다.

  • 매크로의 # 연산자는 토큰을 문자열로 변환하는 역할을 한다.

TypeInfo

class TypeInfo
{
public:
	template <typename T>
	explicit TypeInfo(const TypeInfoInitializer<T>& initializer)
		: name(initializer.name)
		, super(initializer.super)
	{}

private:
	const char* name = nullptr;
	const TypeInfo* super = nullptr;
};

const TypeInfo* super = nullptr; 부분을 주목하자. 부모의 TypeInfo 에 대한 주소를 가지고 있는 부분이다. 이를 통해 TypeInfo 는 타입의 상속 관계를 표현하지만, 다중 상속을 지원하지 않는다는 제약이 있다.

explicit 키워드?

암시적 형변환을 방지하기 위해 사용되며, 명시적인 변환만 허용한다.

TypeInfoInitializer

// ---------------------------------------
//				Type Info
// ---------------------------------------

class TypeInfo;

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

template <typename T>
struct TypeInfoInitializer
{
	TypeInfoInitializer(const char* name)
		: name(name)
	{
		if constexpr (HasSuper<T>)
		{
			super = &(typename T::Super::StaticTypeInfo());
		}
	}

	const char* name = nullptr;
	const TypeInfo* super = nullptr;
};

TypeInfo 의 초기화에 쓰이는 TypeInfoInitializer 구조체이다. C++20의 conceptconstexpr if 를 통해 부모 타입의 TypeInfo 를 얻어오도록 되어있다.

concept

C++20에서 도입된 기능으로, 템플릿을 사용할 때 컴파일 타임에 타입을 검사하여 올바른 타입만 허용하도록 하는 역할을 한다.

if constexpr?

C++17 이후, if constexpr 을 사용해 조건을 컴파일 시점에 평가할 수 있다.

// 타입 T가 내부적으로 T::Super라는 타입 별칭을 가지고 있으며,
// 그 타입이 void가 아닌지를 컴파일 시점에 검사해주는 코드
template <typename T>
concept hasSuper = requires { typename T::Super; } && !std::same_as<typename T::Super, void>;

requires { typename T::Super; }
requires 표현식에서 {} 는 컴파일 시점 요구사항 블록을 정의하는데 사용된다. 블록 안에는 식이 들어가며, 특정 타입이 존재하는지, 특정 멤버가 있는지 등을 컴파일러가 검사한다. 만약 T::Super 가 없으면 컴파일 오류가 발생하지만, requires 표현식 내에서는 오류 대신 해당 조건이 false 로 평가된다.

!std::same_as<typename T::Super, void>;
T::Super 타입이 void 와 같지 않아야 한다는 추가 조건. 즉, T::Supervoid 인 경우도 거짓으로 처리한다. std::same_as<A,B> 는 두 타입이 동일한지를 확인하는데, 단순히 true 또는 false 를 반환하는 간단한 논리 연산이다.

다시 정리하자면, TypeInfo 의 초기화에 쓰이는 TypeInfoInitializer 의 생성자에서는 컴파일 타임에 부모 타입이 있는지를 검사하여 만약 부모 타입이 존재한다면 super 변수에 부모 타입의 TypeInfo 를 받아오게 된다. 없다면 nullptr 이다.

전체 코드 및 테스트

Reflection.h

#pragma once

// ---------------------------------------
//		Super Class Type Deduction
// ---------------------------------------

template <typename T, typename U = void>
struct SuperClassTypeDeduction
{
	using Type = void;
};

template <typename T>
struct SuperClassTypeDeduction<T, std::void_t<typename T::ThisType>>
{
	using Type = T::ThisType;
};

// ---------------------------------------
//				Type Info
// ---------------------------------------

class TypeInfo;

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

template <typename T>
struct TypeInfoInitializer
{
	TypeInfoInitializer(const char* name)
		: name(name)
	{
		if constexpr (HasSuper<T>)
		{
			super = &(typename T::Super::StaticTypeInfo());
		}
	}

	const char* name = nullptr;
	const TypeInfo* super = nullptr;
};

class TypeInfo
{
public:
	template <typename T>
	explicit TypeInfo(const TypeInfoInitializer<T>& initializer)
		: name(initializer.name)
		, super(initializer.super)
	{}

private:
	const char* name = nullptr;
	const TypeInfo* super = nullptr;
};

// ---------------------------------------
//			GENERATED_BODY MACRO
// ---------------------------------------

#define GENERATED_BODY(TypeName) \
public: \
	using Super = typename SuperClassTypeDeduction<TypeName>::Type; \
	using ThisType = TypeName; \
\
	static TypeInfo& StaticTypeInfo() \
	{ \
		static TypeInfo typeInfo { TypeInfoInitializer<ThisType>( #TypeName ) }; \
		return typeInfo; \
	} \
\
	const TypeInfo& GetTypeInfo() const \
	{ \
		return typeInfo; \
	} \
private: \

Main.cpp

#include <iostream>
#include "Reflection.h"

class SuperClass
{
    GENERATED_BODY(SuperClass)
};

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

int main()
{
    MyClass myClass;
    auto Info = myClass.GetTypeInfo();
}

조사식을 이용해 MyClassTypeInfo 를 보자. MyClass 의 name은 "MyClass", super 클래스는 "SuperClass" 로 정상적으로 클래스 타입 정보를 받아오는 것을 확인할 수 있다.

0개의 댓글