WONMOCK LEE / GRAPHICS ENGINEER
프로젝트 04 · 독립 저장소

C++17 Reflection

GENERATE / PROPERTY / METHOD — Unreal의 UCLASS/UPROPERTY/UFUNCTION 참고함. C++17 inline static로 타입/멤버/Method를 컴파일 타임에 등록하고, 헤더만으로 완결되어 링커 에러가 없음.

01 / INTRO

핵심 내용

왜 직접 만들었는지: C++ 표준 RTTI는 타입 이름과 다운캐스트 정도만 줄 뿐 프로퍼티 목록을 안 줘서 GC 순회·직렬화에 못 씀 — Unreal의 UHT 같은 별도 코드 생성 툴 없이, Macro와 Template만으로 컴파일 타임에 타입 정보를 등록하는 STL 비의존 RTTI 대체 시스템을 택함.
단일 상속 체인만 지원그래서 안전한 Cast<T>
TypeInfo는 Class당 SuperType 하나만 모델링함. 위로 올라가는 경로가 하나뿐이라 Cast<T>는 DAG 탐색이 아니라 체인을 걷는 것으로 끝남 — dynamic_cast가 전혀 필요 없음.
힙 할당 0의 Container 순회
Iterator가 64바이트 고정 버퍼에 실제 STL/wtr 반복자를 placement-new로 생성 — vtable 대신 함수 Pointer 6개로 디스패치함.
같은 시그니처 함수마다 별도 static 객체
MethodCreator<Func, func>가 함수 Pointer "값"을 비타입 Template 파라미터로 받음 — 시그니처가 같아도 함수마다 별도 Template Instance → 별도 static이 생김.

특징

Macro 하나로 없앤 보일러플레이트
GENERATE/PROPERTY/METHOD만 붙이면 등록 코드를 손으로 안 써도 됨 — 컴파일 타임 빠른 경로와 런타임 SuperType 워크를 나눠서 평소엔 dynamic_cast보다 빠름.
설계 단계부터 줄인 런타임 오버헤드
64바이트 SBO Iterator(힙 할당 0), 비타입 Template 파라미터로 new 없는 정적 MethodCall 등 — "Reflection은 느리다"는 통념을 구조적으로 반박하려는 선택들임.
명확한 Get/Set 타입 안전성 기준
Get은 상속 관계를 허용하고 Set은 완전히 같은 타입만 허용 — object slicing이나 의도치 않은 다형성 허용 같은 위험을 원천 차단하는 규칙이 문서에 남아 있음.

주요 내용

크로스플랫폼 검증 부재수정됨 · #31
Utils.cpp가 Windows 아닐 때 GetName 대신 이름이 다른 GetClassName을 정의해서, GCC/Clang 빌드는 TypeInfo::Get<T>()를 쓰는 순간 링크가 실패했음 → 2026.08 비Windows 분기 이름을 GetName으로 맞춤.
Property보다 느슨하던 Method 타입 검증수정됨 · #32
MethodInfo::Invoke가 소유 Class를 검증하지 않아서, 시그니처만 같으면 서로 무관한 Class끼리도 조용히 타입 펀될 수 있었음 → Invoke 전에 Instance의 런타임 타입을 소유 Class와 대조하도록 바꿈.
Template Class 조합에서 깨질 수 있는 Macro
Template Class 내부의 PROPERTY()가 GCC/Clang의 공격적인 dead-code elimination에 스트리핑되는 문제가 아직 완전히 해결되지 않음(이슈 #25).
02 / MODULES

상세 내용 & 다이어그램

클래스 다이어그램 — 등록 구조
Reflection 클래스 다이어그램
시퀀스 다이어그램 — 매크로 등록부터 런타임 Cast/조회까지
Reflection 시퀀스 다이어그램

사용 예시

class ObjectB : public ObjectA
{
    GENERATE(ObjectB);
public:
    PROPERTY(m_intValue);
    int m_intValue = 0;

    PROPERTY(m_array);
    wtr::DynamicArray<int> m_array; // Container도 자동 감지

    METHOD(Add);
    int Add(int a, int b) const { return a + b; }
};

// 런타임 조회 — 이름 문자열로 Property/Method에 접근
const auto* type = obj->GetTypeInfo();
const auto* prop = type->GetProperty("m_intValue");
prop->Set(obj, 42);            // 컴파일 타임 오프셋으로 직접 메모리 접근, dynamic_cast 없음

const auto* method = type->GetMethod("Add");
int result = method->Invoke<int>(obj, 3, 4);  // MethodCreator가 함수 Pointer별 static 생성
03 / HISTORY

개발 히스토리

커밋 기록을 불러오는 중…

  1. 1기TypeCast부터2024.01 — 2025.02
  2. 2024.04.02
    ReflectionTypeCast 완성
    첫 커밋(2024.01.14, 오타로 "Reflectin System")부터 TypeCast 하나를 완성하는 데 약 3개월 — 프로퍼티 등록보다 "안전하게 다운캐스트하는 것"을 먼저 풀 문제로 봤다는 뜻.
  3. 2025.02.02
    ReflectionC++11 호환을 위해 표준 기능을 직접 재구현
    std::void_t/std::remove_pointer_t 같은 C++14/17 기능을 Template으로 다시 짜서 C++11에서도 돌아가게 함. 이때 코드도 TypeReflection/MethodReflection/PropertyReflection 세 폴더로 처음 나뉨.
  4. 2기TypeManager 구조 → 대규모 재정리2025.10 — 2025.12
  5. 2025.10.15
    ReflectionTypeInfo + TypeManager 구조 도입
  6. 2025.11.01
    Reflection"Property Set, Get Error Fix"라는 이름의 대규모 재정리
    커밋 메시지는 버그 수정 하나지만 실제 diff는 13개 파일, 수백 줄 — 폴더 구조가 지금의 include/Type, Property, Method/...로 바뀐 것도 이 커밋. 버그를 고치면서 구조 자체를 다시 정리함.
  7. 2025.12.01
    Reflection타입 안전성 우회 경로와 판별 기준을 분리
    raw getter/setter(타입 체크를 건너뛰는 접근)를 넣은 지(11.26) 닷새 뒤 IsBase Template을 따로 도입 — "Get은 상속 허용, Set은 완전히 같은 타입만" 규칙이 이 두 커밋 사이에서 갈라져 나옴.
  8. 2025.12.10
    ReflectionContainer 프로젝트를 submodule로 결합
    external/Container를 붙이고 ContainerPropertyInfo를 새로 만듦 — 64바이트 SBO Iterator도 이 뒤에 자리 잡음.
  9. 2025.12.12
    ReflectionGCC 경고 수정좁았던 수정 범위
    ContainerPropertyInfo.h 한 곳만 건드려서 Utils.cpp의 플랫폼 분기 문제는 그대로 남음 → 2026.08 #31로 마저 고침.
  10. 2기코드 리뷰 → 수정2026
  11. 2026.04.12
    Reflection전방 선언 타입에서 TypeInfo 초기화가 실패하던 문제 수정 (#29)
  12. 2026.08.07
    Reflection리뷰로 찾은 버그 2건 수정
    Windows 밖에서 GetName이 GetClassName으로 남아 링크가 깨지던 문제(#31), MethodInfo::Invoke가 Instance의 실제 타입을 소유 Class와 대조하지 않던 문제(#32) (PR #33).
04 / DESIGN

매크로가 실제로 확장되는 방식

GENERATE(ClassName)

SuperType 탐지
Utils::TypeDetector의 SFINAE로 베이스의 Reflection 태그를 찾아 SuperType으로 alias함. ThisType은 Class 자신을 가리킴.
GetTypeInfo() Override
함수 지역 static const TypeInfo*("magic static", Thread 안전, 최초 1회만 초기화)를 Override된 virtual이 반환함 — 베이스 Pointer가 런타임에 실제 파생 타입을 복원하는 방법.
자체 등록
inline static const TypeInfo* s_typeInfo = GetStaticTypeInfo(); — 정적 초기화 시점에 동적 초기자가 실행되고, TypeInfo 생성자 마지막에서 TypeManager::Regist(this)를 호출해 unordered_map 에 등록함. TypeManager::GetHandle()이 Meyer's Singleton이라 TU 간 정적 초기화 순서 문제에서 자유로움.
이름 붙은 링커 위험 요소
__STATIC_USED__(GCC/Clang의 __attribute__((used))) 가 "참조되지 않는" static이 스트리핑되는 걸 막으려고 존재 — 늘 이기는 건 아님(아래 이슈 참고).

PROPERTY(member)

오프셋 캡처
offset = reinterpret_cast<size_t>(&(static_cast<Class*>(nullptr)->*pointer)) — null 베이스 Pointer를 통해 멤버 Pointer를 역참조함. vtable이 있는(non-standard-layout) Class에서는 offsetof가 지원되지 않아 필요한 방식.
Get/Set
둘 다 reinterpret_cast<char*>(instance) + m_propertyOffset 으로 해석됨. 대입은 캡처 없는 람다가 void(*)(void*, const void*) 썽크로 decay되어 *(Property*)dest = *(const Property*)src를 수행.
Sequence 다이어그램 — Set()의 타입 안전성 분기(Pointer는 상속 허용, 값 타입은 완전 일치만)
Property Set 타입 안전성 시퀀스 다이어그램

METHOD(name)

Type Erasure로 지운 호출 대상
MethodBase(빈 껍데기) → MethodCall<Return, Args...>(순수 가상 Invoke) → MemberMethodCall/ StaticMethodCall(실제 함수 Pointer 보관) 3단 상속으로, 호출부는 Return/Args만 알면 됨.
MethodCreator<Func, func>함수 Pointer "값"을 받는 Template 파라미터
같은 시그니처의 함수라도 func이 다른 값이면 서로 다른 Template Instance — 함수마다 자기 static MemberMethodCall을 따로 가짐(부분 특수화 3개: 멤버/const 멤버/정적 함수).
Class 다이어그램 — MethodBase 상속 구조와 MethodCreator
Method 디스패치 클래스 다이어그램

Zero-allocation Container Iterator

vtable · 베이스 Class 없는 구조
alignas(void*) uint8_t m_storage[64]에 함수 Pointer 6개 (Next/Prev/Destroy/Element/Copy/Compare) — Container 타입별로 고정된 이 함수 Pointer 묶음 자체가 수동으로 만든 vtable임.
생성
begin()이 실제 STL/wtr const_iterator를 64바이트 버퍼에 placement-new로 직접 생성함.
진짜 깊은 복사
버퍼를 그냥 memcpy하면 내부 Pointer(해시 버킷, 리스트 Node)를 들고 있는 반복자가 깨짐. CopyFunc가 원본에서 새로 placement-new함.
명시적 소멸
uint8_t[]는 소멸자를 자동으로 안 불러줌 — ~Iterator()가 DestroyFunc를 호출해 그 자리에서 실제 반복자의 소멸자를 실행함.
05 / ISSUES

이슈

- 원인: PROPERTY()가 Template Class 내부에 만드는 static 객체가, Template이 겉보기에 안 쓰이는 것처럼 보이면 GCC/Clang의 공격적인 dead-code elimination에 의해 조용히 스트리핑되어 Property/GC 등록이 깨짐.
- 개선 방향: 컴파일러 used/retain 속성으로 강제 보존 — __STATIC_USED__가 노리는 것과 같은 종류의 해법이지만 아직 Template까지는 완전히 커버하지 못함.
- 원인: TypeInfo<T>는 Metadata 추출을 위해 컴파일 타임에 완전한 타입의 크기/정렬 정보가 필요한데, 전방 선언만으로는 이걸 제공할 수 없음.
06 / BUGS

버그

실제 버그 3건 — 2건 수정 완료 클릭해서 펼치기
#31 · src/Utils.cpp:9,53, include/Type/TypeInfo.h:93 Windows 밖에서의 링크 실패GetName 정의 누락 해결
- TypeInfo.h:93은 플랫폼 무관하게 Utils::GetName(...)을 호출하는데, Utils.cpp는 #if defined(_WIN32)일 때만 GetName을 정의하고 #else 분기는 이름이 다른 GetClassName을 정의함. GCC/Clang 빌드는 TypeInfo::Get<T>()를 Instance화하는 순간 "undefined reference"로 링크 실패 — #25/#29와는 별개인, 아직 등록되지 않은 빌드 차단 버그.
- 수정(2026.08.07): 비Windows 분기의 함수 이름을 GetName으로 맞춤 (PR #33).
#32 · include/Method/MethodInfo.h:77, MethodCall.h:117 MethodInfo::Invoke의 소유 Class 검증 누락 해결
- 시그니처(Return/Args)가 같으면 서로 무관한 두 Class의 Method가 같은 MethodCall<Return,Args...> TypeInfo로 묶여 Cast를 통과함. MemberMethodCall::Invoke는 전달받은 Pointer를 그냥 static_cast<ClassType*>해서 호출 — PropertyInfo와 달리 런타임 타입 체크가 없어서, 잘못된 Class의 Instance를 넘기면 조용히 타입 펀됨.
- 수정(2026.08.07): Invoke 전에 Instance의 런타임 타입을 소유 Class와 대조해서 무관한 Class면 거부함 (PR #33).
include/Type/TypeInfo.h:110, src/Type/TypeManager.cpp:34-37 TypeManager map 키사실상 Pointer 자기 자신 + 락 없는 조회 설계 노트
- m_typeHash는 reinterpret_cast<size_t>(this) — 이름 기반 해시가 아니라 TypeInfo 객체 자신의 메모리 주소임. ASLR 때문에 프로세스마다 값이 달라져 직렬화/프로세스 간 식별에는 못 씀. GetTypeMap()도 락 없이 원본 map을 그대로 반환해서, Regist()가 거는 뮤텍스와 별개로 순회 중 경합이 가능함.

GitHub에 지금 열려 있는 이슈 (실시간)

불러오는 중…