리플렉션 실전 — 매크로 없이 구조체를 순회하는 법
목표: 매크로 없이 구조체를 JSON으로 바꾸기
앞 장에서 ^^와 [: :]가 서로 역연산이라는 것까지 봤습니다. 이제 이걸로 실제로 쓸모 있는 걸 만들어 보겠습니다. 목표는 단순합니다 — 어떤 구조체든 필드 이름과 필드 수를 나열하지 않고도 JSON 문자열로 바꾸는 함수를 짜는 것입니다.
struct Point { int x; int y; };
struct User { std::string name; int age; bool active; };
// 목표: 아래처럼 구조체마다 매크로 나열 없이 동작해야 한다
std::string json1 = to_json(Point{3, 4});
std::string json2 = to_json(User{"Alice", 30, true});members_of로 필드 목록 얻기
std::meta::members_of(r)는 타입 r이 가진 멤버들의 리플렉션 값을 벡터로 돌려줍니다. 여기에 nonstatic_data_members_of를 쓰면 정적 멤버나 함수는 걸러내고 인스턴스 필드만 남습니다.
#include <meta>
#include <string>
#include <format>
template <typename T>
consteval auto member_infos() {
return std::meta::nonstatic_data_members_of(^^T);
}이 시점에서 우리가 가진 건 "필드가 몇 개 있고 각각 어떤 리플렉션 값인지"라는 컴파일 타임 정보입니다. 이걸 런타임 루프로 처리할 수는 없습니다 — 필드마다 타입이 다르기 때문에 일반적인 for 루프로는 순회가 안 됩니다. 여기서 C++26이 함께 도입한 확장 문(expansion statement), template for가 필요합니다.
template for — 서로 다른 타입의 멤버를 순회하는 루프
template <typename T>
std::string to_json(const T& obj) {
std::string out = "{";
bool first = true;
template for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T)) {
if (!first) out += ",";
first = false;
out += "\"" + std::string(std::meta::identifier_of(member)) + "\":";
// member 를 실제 멤버 접근 표현식으로 되돌린다 (스플라이스)
const auto& value = obj.[:member:];
if constexpr (std::same_as<std::remove_cvref_t<decltype(value)>, std::string>) {
out += "\"" + value + "\"";
} else if constexpr (std::same_as<std::remove_cvref_t<decltype(value)>, bool>) {
out += value ? "true" : "false";
} else {
out += std::format("{}", value);
}
}
out += "}";
return out;
}template for의 몸통은 일반 런타임 루프처럼 보이지만, 사실은 각 멤버마다 컴파일러가 코드를 한 번씩 찍어내는 것에 가깝습니다. Point는 몸통이 두 번, User는 세 번 전개됩니다. 그래서 if constexpr로 멤버 타입에 따라 분기해도 문제가 없습니다 — 각 전개마다 member가 상수이므로 컴파일러 입장에서는 매번 다른 타입을 상대하는 완전히 별개의 코드입니다.
obj.[:member:]가 이 장의 핵심입니다. member는 "Point의 x 필드"라는 정보를 담은 컴파일 타임 값일 뿐인데, [: :]로 스플라이스하면 그게 실제 obj.x 접근 표현식으로 되돌아갑니다. 필드 이름을 문자열로 하드코딩한 적이 한 번도 없다는 걸 눈여겨보세요 — identifier_of(member)가 필드 이름 자체를 컴파일 타임에 뽑아줍니다.
이게 실무에서 뭘 대체하는가
이 패턴이 실제로 대체하는 건 세 부류입니다.
- 직렬화 매크로 (
NLOHMANN_DEFINE_TYPE_INTRUSIVE류) — 필드를 추가해도 매크로 목록을 안 고쳐도 됩니다. - ORM 매핑 코드 생성기 — 컴파일 전에 별도 도구를 돌려 매핑 코드를 만드는 대신, 컴파일 자체가 매핑을 만듭니다.
operator<<·operator==반복 작성 — 구조체마다 손으로 짜던 비교·출력 연산자를 리플렉션으로 한 번만 짜서 모든 타입에 재사용할 수 있습니다.
다만 이 예제가 프로덕션 라이브러리 수준은 아니라는 점은 밝혀둬야겠습니다. 중첩 구조체, std::optional, 컨테이너 필드를 다루려면 재귀와 개념(concept) 제약이 훨씬 더 필요합니다. 실제로 Boost나 여러 오픈소스 팀이 이 표준을 기반으로 한 정식 직렬화 라이브러리를 이미 준비 중이라, 이 책이 나올 즈음엔 손으로 이런 코드를 짤 필요조차 없어질 가능성이 높습니다. 그래도 이 기초를 이해해두면 나중에 그 라이브러리들이 내부에서 뭘 하는지 읽을 수 있습니다.