C++17 Reflection
GENERATE / PROPERTY / METHOD
— Unreal의 UCLASS/UPROPERTY/UFUNCTION
참고함. C++17 inline static로 타입/멤버/Method를 컴파일 타임에
등록하고, 헤더만으로 완결되어 링커 에러가 없음.
목차
- 01핵심 내용
- 02상세 내용 & 다이어그램
- 03개발 히스토리
- 04매크로가 실제로 확장되는 방식
- 05이슈
- 06버그
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
그래서 안전한 Cast<T>
상세 내용 & 다이어그램
단일 상속만 지원그래서 안전한 Cast<T>
클래스 다이어그램 — 등록 구조
시퀀스 다이어그램 — 매크로 등록부터 런타임 Cast/조회까지
사용 예시
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
커밋 메시지 · diff에서 그대로 확인한 순서
개발 히스토리
2024.01 첫 커밋부터 실제 커밋 로그 기준커밋 메시지 · diff에서 그대로 확인한 순서
커밋 기록을 불러오는 중…
- 1기TypeCast부터2024.01 — 2025.02
- 2024.04.02ReflectionTypeCast 완성첫 커밋(2024.01.14, 오타로 "Reflectin System")부터
TypeCast하나를 완성하는 데 약 3개월 — 프로퍼티 등록보다 "안전하게 다운캐스트하는 것"을 먼저 풀 문제로 봤다는 뜻. - 2025.02.02ReflectionC++11 호환을 위해 표준 기능을 직접 재구현
std::void_t/std::remove_pointer_t같은 C++14/17 기능을 Template으로 다시 짜서 C++11에서도 돌아가게 함. 이때 코드도TypeReflection/MethodReflection/PropertyReflection세 폴더로 처음 나뉨. - 2기TypeManager 구조 → 대규모 재정리2025.10 — 2025.12
- 2025.10.15ReflectionTypeInfo + TypeManager 구조 도입
- 2025.11.01Reflection"Property Set, Get Error Fix"라는 이름의 대규모 재정리커밋 메시지는 버그 수정 하나지만 실제 diff는 13개 파일, 수백 줄 — 폴더 구조가 지금의
include/Type, Property, Method/...로 바뀐 것도 이 커밋. 버그를 고치면서 구조 자체를 다시 정리함. - 2025.12.01Reflection타입 안전성 우회 경로와 판별 기준을 분리raw getter/setter(타입 체크를 건너뛰는 접근)를 넣은 지(11.26) 닷새 뒤
IsBaseTemplate을 따로 도입 — "Get은 상속 허용, Set은 완전히 같은 타입만" 규칙이 이 두 커밋 사이에서 갈라져 나옴. - 2025.12.10ReflectionContainer 프로젝트를 submodule로 결합
external/Container를 붙이고ContainerPropertyInfo를 새로 만듦 — 64바이트 SBO Iterator도 이 뒤에 자리 잡음. - 2025.12.12ReflectionGCC 경고 수정좁았던 수정 범위
ContainerPropertyInfo.h한 곳만 건드려서Utils.cpp의 플랫폼 분기 문제는 그대로 남음 → 2026.08 #31로 마저 고침. - 2기코드 리뷰 → 수정2026
- 2026.04.12Reflection전방 선언 타입에서 TypeInfo 초기화가 실패하던 문제 수정 (#29)
- 2026.08.07Reflection리뷰로 찾은 버그 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는 상속 허용, 값 타입은 완전 일치만)
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
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건 수정 · 상태는 실시간 동기화
버그
2026.08 코드 리뷰에서 직접 찾아 등록한 이슈3건 중 2건 수정 · 상태는 실시간 동기화
실제 버그 3건 — 2건 수정 완료 클릭해서 펼치기
-
- 수정(2026.08.07): 비Windows 분기의 함수 이름을
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).
- 시그니처(Return/Args)가 같으면 서로 무관한 두 Class의 Method가 같은
- 수정(2026.08.07): Invoke 전에 Instance의 런타임 타입을 소유 Class와 대조해서 무관한 Class면 거부함 (PR #33).
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에 지금 열려 있는 이슈 (실시간)
불러오는 중…