C++ 컴파일 타임 리플렉션 | C++26 Reflection·magic_enum·매크로 직렬화·검증

들어가며: “구조체 멤버를 자동으로 순회하고 싶어요”

왜 컴파일 타임 리플렉션이 필요한가?

게임 엔진에서 Transform, RigidBody 같은 컴포넌트를 JSON으로 직렬화하거나, 설정 로더에서 YAML 키를 구조체 멤버에 자동 매핑하거나, API 검증에서 필수 필드가 비어 있는지 확인할 때—멤버 이름·타입·개수를 컴파일 타임에 알아야 반복 코드 없이 자동화할 수 있습니다. C++에는 Java나 C#처럼 런타임 리플렉션이 표준으로 없어서, 지금까지는 수동 등록, 매크로, 코드 생성에 의존해 왔습니다. 비유하면: “도서관의 모든 책 목록을 자동으로 카탈로그화하는 시스템”이 있는데, C++에는 그런 카탈로그가 없어서 책마다 직접 목록을 손으로 적어 두는 상황입니다. 컴파일 타임 리플렉션이 있으면, 타입 선언만으로 멤버 정보를 자동으로 추출할 수 있습니다.

flowchart LR
  subgraph problem[문제 상황]
    P1[User 구조체] --> P2[to_json 수동 작성]
    P3[Order 구조체] --> P4[to_json 수동 작성]
    P2 --> P5[멤버 추가 시 누락 위험]
    P4 --> P5
  end
  subgraph solution[컴파일 타임 리플렉션]
    S1[타입 선언] --> S2[^^T 또는 magic_enum]
    S2 --> S3[멤버 자동 추출]
    S3 --> S4[직렬화·검증 자동화]
  end

목표:

  • C++26 Reflection (P2996): ^^ 연산자, std::meta::info, 스플라이싱 [: :]

  • 현재 워크어라운드: magic_enum, 매크로 기반 멤버 순회

  • 완전한 예제: 직렬화, 검증, enum-to-string

  • 일반적인 에러와 베스트 프랙티스

  • 프로덕션 패턴: 설정 로드, 프로토콜 직렬화 이 글을 읽으면:

  • C++26 리플렉션 문법과 사용 예를 이해할 수 있습니다.

  • C++26 미지원 환경에서 magic_enum·매크로로 대체할 수 있습니다.

  • 직렬화·검증을 자동화하는 패턴을 적용할 수 있습니다.


구조체마다 to_json·from_json을 손으로 쓰는 문제

게임 엔진 컴포넌트 직렬화

Transform, RigidBody, Mesh 등 수십 개 컴포넌트를 JSON으로 저장·로드할 때, 각 타입마다 to_json, from_json을 수동 작성하면 멤버 추가·삭제 시 누락 위험이 큽니다.

// ❌ 컴포넌트 10개면 to_json/from_json 20개 함수, 멤버 추가 시 모두 수정
struct Transform { float x, y, z; float rx, ry, rz; };
std::string to_json(const Transform& t) { /* 6개 필드 수동 */ }

설정 파일 로드

YAML/JSON 설정을 GameConfig, NetworkConfig 구조체로 로드할 때, 키 이름과 멤버를 매칭하려면 멤버 이름을 알아야 합니다. 리플렉션이 없으면 매크로나 수동 매핑이 필요합니다.

// ❌ "port", "host" 등 키 이름 하드코딩, 멤버 추가 시 수동 수정
void load_config(GameConfig& c, const YAML::Node& n) {
    c.port = n["port"].as<int>(); c.host = n["host"].as<std::string>();
}

주의사항: 오타나 키 이름 변경은 런타임에만 드러나는 경우가 많아, 테스트나 스키마 검증을 병행하세요. enum을 문자열로 변환

Status::Running을 "Running"으로, "Running"을 Status::Running으로 변환할 때, switch-case를 수동으로 작성하면 enum 값 추가 시 누락이 발생합니다.

// ❌ enum 값 추가 시 to_string·from_string 둘 다 수정
// 변수 선언 및 초기화
const char* to_string(Status s) {
    switch (s) { case Status::Idle: return "Idle"; case Status::Running: return "Running"; /* ... */ }
}

API 요청 검증

REST API에서 User 객체를 받을 때 name, email이 필수인지 검증하려면, 필수 필드 목록을 런타임에 알아야 합니다. 리플렉션으로 “필수 필드” 메타데이터를 붙이면 자동 검증이 가능합니다.

// ❌ 필수 필드 추가 시 validate_user 수동 수정
bool validate_user(const User& u) { return !u.name.empty() && !u.email.empty(); }

프로토콜 패킷 직렬화

네트워크 패킷 구조체를 바이너리로 변환할 때, 멤버 순서·타입이 바뀌면 수동 직렬화 코드도 함께 수정해야 합니다. 컴파일 타임에 멤버를 순회하면 자동화할 수 있습니다.

// ❌ 멤버 순서·타입 변경 시 serialize/deserialize 수동 수정
struct Packet { uint16_t id; uint32_t len; uint8_t flags; };
void serialize(const Packet& p, uint8_t* buf) { memcpy(buf, &p.id, 2); /* ... */ }

테스트/스키마 생성

구조체 멤버 목록을 기반으로 JSON 스키마, OpenAPI 스펙을 생성할 때, 멤버 정보가 없으면 코드 생성 도구에 의존하거나 수동 유지보수가 필요합니다.

문제의 원인

원인설명
표준 리플렉션 부재C++에는 표준 타입·멤버 조회 API가 없음
수동 등록 부담구조체마다 Getter/Setter 등록 필요
enum 문자열 변환__PRETTY_FUNCTION__ 등 컴파일러 확장에 의존
멤버 변경 시 동기화직렬화 코드와 구조체 변경이 분리되어 누락 위험

해결 방향

  • C++26: std::meta::*로 타입·멤버를 컴파일 타임에 조회
  • 현재: magic_enum(enum 전용), 매크로(구조체 멤버), 코드 생성

C++26 Reflection (P2996) 개요

C++26에 채택된 리플렉션

2025년 6월 WG21 회의에서 P2996 “Reflection for C++26”이 채택되었고, 함께 쓰는 제안들도 같이 들어갔습니다. 멤버 목록을 순회하며 코드를 펼치는 확장 문 template for(P1306), 컴파일 타임 벡터를 정적 배열로 바꾸는 std::define_static_array(P3491), 필드에 값을 붙이는 어노테이션 [[=...]](P3394), 접근 권한을 명시하는 std::meta::access_context(P3547)입니다. 표준화 과정에서 API가 여러 번 바뀌어서, 인터넷의 예제 중 std::meta::name_v<^^T>나 nonstatic_data_members_of<^^T> 같은 템플릿 변수 형태, 일반 for (constexpr ...) 루프, consteval 변수 선언은 채택된 문법이 아니며 컴파일되지 않습니다. 이 글의 C++26 예제는 채택된 형태로 작성했습니다.

핵심 개념

도구역할예
^^엔티티를 컴파일 타임 값 std::meta::info로constexpr auto r = ^^User;
메타함수 (consteval)info로 이름·타입·멤버 목록 조회identifier_of(r), type_of(r), nonstatic_data_members_of(r, ctx), enumerators_of(r)
[: r :] (스플라이스)info를 다시 코드로obj.[:m:], typename [:t:]
template for컴파일 타임 범위를 원소마다 펼치기멤버마다 직렬화 코드 생성
[[=v]]필드에 값 어노테이션[[=skip]] std::string cache;

std::meta::info 값은 컴파일 타임에만 존재하므로 변수는 constexpr로 선언하고, 이를 다루는 함수는 상수 평가 문맥에서 호출됩니다. 기초 문법은 C++26 리플렉션 기초(#26-1)에 따로 정리했고, 이 글은 직렬화·검증에 쓰는 방법과 C++26 이전의 대안에 집중합니다.

#include <meta>

struct Point { int x; int y; };

static_assert(std::meta::identifier_of(^^Point) == "Point");
static_assert(std::meta::identifier_of(^^Point::x) == "x");
static_assert(std::meta::type_of(^^Point::x) == ^^int);

access_context

멤버를 나열하는 메타함수는 어떤 위치에서 접근 가능한 멤버를 보고 싶은지를 두 번째 인자로 받습니다. access_context::current()는 호출한 위치에서 접근 가능한 멤버만, access_context::unchecked()는 private까지 전부를 돌려줍니다. 리플렉션이 캡슐화를 우회하는 도구가 되지 않도록, private을 보려면 코드에 그 의도를 명시하게 한 것입니다.

C++26 리플렉션 흐름

flowchart LR
  A["struct User"] -->|"^^User"| B["std::meta::info"]
  B -->|"nonstatic_data_members_of"| C["멤버 목록"]
  C -->|"template for"| D["멤버마다 obj.[:m:] 코드 생성"]

기능 테스트 매크로

#if defined(__cpp_impl_reflection)
  #include <meta>
  #define HAS_STD_REFLECTION 1
#else
  #define HAS_STD_REFLECTION 0
#endif

2026년 기준으로 GCC 16이 -std=c++26 -freflection으로 구현을 제공하고, Clang은 Bloomberg의 clang-p2996 포크를 Compiler Explorer에서 쓸 수 있습니다. MSVC는 아직 공개된 구현이 없으므로, 여러 컴파일러를 지원하는 코드는 이 매크로로 대체 경로를 둡니다. 초기 구현의 매크로 값은 버전마다 다를 수 있어 특정 숫자와 비교하기보다 정의 여부로 분기하는 편이 안전합니다.


C++26 리플렉션으로 직렬화·필드 검증·enum 반사

예제 1: 자동 JSON 직렬화

#include <meta>
#include <string>
#include <type_traits>

inline std::string quote(std::string_view s) { return "\"" + std::string(s) + "\""; }

template <typename T>
std::string to_json(const T& obj) {
    std::string out = "{";
    bool first = true;
    constexpr auto ctx = std::meta::access_context::unchecked();
    template for (constexpr auto m :
                  std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
        if (!first) out += ",";
        first = false;
        out += quote(std::meta::identifier_of(m)) + ":";
        using M = std::remove_cvref_t<decltype(obj.[:m:])>;
        if constexpr (std::is_same_v<M, std::string>) out += quote(obj.[:m:]);
        else if constexpr (std::is_same_v<M, bool>)    out += obj.[:m:] ? "true" : "false";
        else if constexpr (std::is_arithmetic_v<M>)    out += std::to_string(obj.[:m:]);
        else                                          out += to_json(obj.[:m:]);   // 중첩 구조체
    }
    return out + "}";
}

struct User { int id; std::string name; std::string email; };
// to_json(User{1, "Alice", "[email protected]"})
// {"id":1,"name":"Alice","email":"[email protected]"}

std::vector<std::meta::info>는 컴파일 타임에만 존재할 수 있어서 template for에 바로 넘길 수 없고, std::define_static_array로 정적 배열로 바꿔야 합니다. 이걸 빠뜨리면 “확장 문의 범위가 상수 표현식이 아니다”는 에러가 나는데, 처음 쓸 때 가장 자주 보는 에러입니다.

예제 2: 필수 필드 검증 (어노테이션)

어노테이션으로 “이 필드는 비어 있으면 안 된다” 같은 규칙을 필드 선언 옆에 붙이고, 검증 함수가 그 어노테이션을 읽게 할 수 있습니다.

#include <meta>
#include <string>
#include <vector>

struct required_t {};
inline constexpr required_t required{};

struct SignupForm {
    [[=required]] std::string email;
    [[=required]] std::string password;
    std::string nickname;          // 선택
};

template <typename T>
std::vector<std::string> missing_required(const T& obj) {
    std::vector<std::string> missing;
    constexpr auto ctx = std::meta::access_context::unchecked();
    template for (constexpr auto m :
                  std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
        if constexpr (!std::meta::annotations_of_with_type(m, ^^required_t).empty()) {
            if (obj.[:m:].empty()) missing.emplace_back(std::meta::identifier_of(m));
        }
    }
    return missing;
}
// missing_required(SignupForm{"", "pw", ""}) → {"email"}

규칙이 필드와 같은 줄에 있으니, 필드를 추가하거나 이름을 바꿔도 검증 목록을 따로 고칠 일이 없습니다.

예제 3: enum 반사

#include <meta>
#include <string_view>
#include <optional>
#include <type_traits>

template <typename E> requires std::is_enum_v<E>
constexpr std::string_view enum_name(E v) {
    template for (constexpr auto e : std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (v == [:e:]) return std::meta::identifier_of(e);
    }
    return "<unknown>";
}

template <typename E> requires std::is_enum_v<E>
constexpr std::optional<E> enum_from_name(std::string_view name) {
    template for (constexpr auto e : std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (name == std::meta::identifier_of(e)) return [:e:];
    }
    return std::nullopt;
}

enum class Status { Idle, Running = 100, Stopped = 5000 };
static_assert(enum_name(Status::Stopped) == "Stopped");   // magic_enum의 값 범위 제한이 없음

C++26 이전의 대안 1: magic_enum

magic_enum이란?

magic_enum은 C++17 헤더 전용 라이브러리로, enum에 대해 컴파일 타임 리플렉션을 제공합니다. 매크로 없이 enum을 문자열로, 문자열을 enum으로 변환할 수 있습니다.

  • 설치: vcpkg vcpkg install magic-enum, 또는 GitHub에서 직접 include
  • 요구: C++17, GCC/Clang/MSVC

enum → 문자열

#include <magic_enum.hpp>
#include <iostream>
enum class Color { Red, Green, Blue };
int main() {
    Color c = Color::Green;
    auto name = magic_enum::enum_name(c);
    std::cout << name << "\n";  // "Green"
    return 0;
}

문자열 → enum

#include <magic_enum.hpp>
#include <iostream>
enum class Status { Idle, Running, Stopped };
int main() {
    auto status = magic_enum::enum_cast<Status>("Running");
    if (status.has_value()) {
        std::cout << "Parsed: " << static_cast<int>(status.value()) << "\n";
    }
    return 0;
}

정수 → enum

auto color = magic_enum::enum_cast<Color>(2);
if (color.has_value()) {
    // color.value() == Color::Blue
}

enum 값 순회

#include <magic_enum.hpp>
#include <iostream>
enum class Color { Red, Green, Blue };
int main() {
    for (auto [value, name] : magic_enum::enum_entries<Color>()) {
        std::cout << static_cast<int>(value) << " -> " << name << "\n";
    }
    return 0;
}

magic_enum 제한사항

제한설명
값 범위기본 -128~127 범위만 스캔 (MAGIC_ENUM_RANGE_MIN/MAX로 조정)
이름 길이__PRETTY_FUNCTION__ 등 컴파일러 출력 길이 제한
중복 값같은 enum 값에 여러 이름이 있으면 첫 번째만 반환
네임스페이스enum이 익명 네임스페이스에 있으면 일부 환경에서 실패

magic_enum 활용: 로깅·직렬화

#include <magic_enum.hpp>
#include <spdlog/spdlog.h>
enum class LogLevel { Debug, Info, Warning, Error };
void log(LogLevel level, const std::string& msg) {
    std::string name = std::string(magic_enum::enum_name(level));
    spdlog::log(static_cast<spdlog::level::level_enum>(static_cast<int>(level)),
                "{}: {}", name, msg);
}
// JSON 직렬화
std::string to_json(LogLevel level) {
    return "\"" + std::string(magic_enum::enum_name(level)) + "\"";
}

magic_enum 완전한 예제: enum_cast + enum_name + enum_entries

#include <magic_enum.hpp>
#include <iostream>
enum class LogLevel { Debug, Info, Warning, Error };
void print_valid_levels() {
    for (auto [v, n] : magic_enum::enum_entries<LogLevel>()) std::cout << n << " ";
}
int main(int argc, char** argv) {
    if (argc < 2) { print_valid_levels(); return 1; }
    auto level = magic_enum::enum_cast<LogLevel>(argv[1]);
    if (!level) { std::cerr << "Invalid. "; print_valid_levels(); return 1; }
    std::cout << magic_enum::enum_name(*level) << "\n";
}

C++26 이전의 대안 2: 매크로 기반 멤버 순회

구조체 멤버를 매크로로 등록

C++26 리플렉션이 없을 때, 매크로로 멤버를 순회하는 코드를 생성할 수 있습니다. BOOST_PP_SEQ_FOR_EACH 또는 std::apply + std::tuple 조합을 사용합니다.

방법 1: std::tuple + std::apply

#include <tuple>
#include <iostream>
#include <string>
struct User {
    int id;
    std::string name;
    std::string email;
};
// 멤버 포인터를 tuple로 묶음
auto as_tuple(User& u) {
    return std::tie(u.id, u.name, u.email);
}
// 순회하며 직렬화 (간단 버전)
template <typename T>
void serialize_members(const T& t) {
    std::apply([&t](const auto&... members) {
        ((std::cout << members << " "), ...);
    }, as_tuple(const_cast<T&>(t)));
}

한계: as_tuple을 수동으로 작성해야 하므로, 멤버 추가 시 누락 위험이 있습니다.

방법 2: REFLECT 매크로

#define REFLECT_MEMBER(Type, Member) \
     { return obj.Member; }
#define REFLECT_STRUCT(Type, ...) \
    struct Type { __VA_ARGS__ }; \
    namespace reflect { \
        template <> struct MembersOf<Type> { \
            static void for_each(const Type& obj, auto&& fn) { \
                fn("id", obj.id); \
                fn("name", obj.name); \
                fn("email", obj.email); \
            } \
        }; \
    }
// 사용 시 매크로 내부에서 멤버를 수동 나열해야 함

방법 3: X-Macro 패턴 (완전한 예제)

X-Macro는 멤버 목록을 한 곳에서 정의하며, 구조체 선언·직렬화·역직렬화·검증 등에서 재사용합니다.

#define CONFIG_FIELDS X(int, port, 8080) X(std::string, host, "localhost") X(bool, ssl, false)
struct Config {
#define X(type, name, default_val) type name;
    CONFIG_FIELDS
#undef X
};
// to_json: CONFIG_FIELDS 기반
std::string to_json(const Config& c) {
    return "{\"port\":" + std::to_string(c.port) + ",\"host\":\"" + c.host + "\",\"ssl\":" + (c.ssl ? "true" : "false") + "}";
}
// from_json: j.contains(#name)로 매핑
void from_json(const nlohmann::json& j, Config& c) {
#define X(type, name, def) if (j.contains(#name)) c.name = j[#name].get<type>();
    CONFIG_FIELDS
#undef X
}

장점: 멤버 추가 시 CONFIG_FIELDS 한 곳만 수정.
단점: 매크로 확장으로 복잡도 증가, #name으로 이름 문자열화.

X-Macro 기본 형태 (User 예제)

// user.h
#define USER_MEMBERS \
    X(int, id) \
    X(std::string, name) \
    X(std::string, email)
struct User {
#define X(type, name) type name;
    USER_MEMBERS
#undef X
};
// user_serialize.cpp
void to_json(const User& u, std::ostream& out) {
    out << "{";
#define X(type, name) out << "\"" #name "\":" << u.name << ",";
    USER_MEMBERS
#undef X
    out << "}";
}

방법 4: 구조체 바인딩 (C++17)

#include <tuple>
#include <iostream>
struct Point {
    int x, y;
};
// 구조체 바인딩으로 멤버 개수·타입 추론
template <typename T>
void print_members(const T& obj) {
    auto [x, y] = obj;
    std::cout << "x=" << x << " y=" << y << "\n";
}
int main() {
    Point p{10, 20};
    print_members(p);
}

한계: 멤버 이름을 런타임에 알 수 없으며, 구조체별로 바인딩 코드가 다릅니다.

완전한 매크로 직렬화 예제

#include <iostream>
#include <sstream>
#include <string>
#define REFLECT_STRUCT(Type, ...) \
    struct Type { __VA_ARGS__ }; \
    inline std::string to_json_impl(const Type& obj, int) { \
        std::ostringstream oss; \
        oss << "{"; \
        return oss.str(); \
    }
// 수동 필드 추가 매크로
#define FIELD_JSON_STR(name) \
    oss << "\"" #name "\":\"" << obj.name << "\""
#define FIELD_JSON_INT(name) \
    oss << "\"" #name "\":" << obj.name
struct User {
    int id;
    std::string name;
    std::string email;
};
std::string to_json(const User& u) {
    std::ostringstream oss;
    oss << "{\"id\":" << u.id
        << ",\"name\":\"" << u.name << "\""
        << ",\"email\":\"" << u.email << "\"}";
    return oss.str();
}
int main() {
    User u{1, "Alice", "[email protected]"};
    std::cout << to_json(u) << "\n";
    return 0;
}

magic_enum 범위, 옛 리플렉션 문법, 멤버 순서 불일치 같은 에러

에러 1: magic_enum이 enum을 찾지 못함

원인: enum 값이 MAGIC_ENUM_RANGE_MINMAGIC_ENUM_RANGE_MAX 범위(-128127 기본)를 벗어남.

// ❌ 잘못된 예
enum class BigEnum { A = 0, B = 1000, C = 2000 };
auto name = magic_enum::enum_name(BigEnum::B);  // 빈 문자열 반환 가능
// ✅ 해결: magic_enum.hpp include 전에 범위 정의
#define MAGIC_ENUM_RANGE_MIN 0
#define MAGIC_ENUM_RANGE_MAX 2048
#include <magic_enum.hpp>

에러 2: enum이 익명 네임스페이스에 있음

원인: __PRETTY_FUNCTION__ 등에서 이름이 컴파일러마다 다르게 나오거나, 링크 시 이름이 없어짐.

// ❌ 나쁜 예
namespace {
enum class Status { Idle, Running };
}
// ✅ 해결: 이름 있는 네임스페이스 또는 전역
namespace app {
enum class Status { Idle, Running };
}

에러 3: C++26 리플렉션 문법 오류

원인: 두 가지가 섞여 있습니다. 하나는 리플렉션을 구현하지 않은 컴파일러(GCC 15 이하, 본가 Clang, MSVC)에서 쓴 경우, 다른 하나는 표준화 이전의 옛 문법 예제를 복사한 경우입니다.

// ❌ 옛 문법: 채택된 C++26에는 없음
consteval auto info = ^^MyStruct;                              // consteval은 변수에 못 붙임
auto name = std::meta::name_v<^^MyStruct>;                    // name_v 없음
for (constexpr auto m : std::meta::nonstatic_data_members_of<^^T>) {}  // 템플릿 형태·일반 for 아님
// ✅ 채택된 문법 + 기능 테스트 매크로로 폴백
#if defined(__cpp_impl_reflection)
constexpr auto info = ^^MyStruct;
constexpr auto name = std::meta::identifier_of(info);
#else
// magic_enum 또는 매크로 사용
#endif

에러 4: 매크로에서 멤버 이름 문자열화 실패

원인: # 연산자는 매크로 인자를 문자열로 바꾸는데, 중첩 매크로나 __VA_ARGS__에서 잘못된 결과가 나올 수 있음.

// ❌ 잘못된 예
#define FIELD(name) #name
#define REFLECT(name) FIELD(name)
REFLECT(user.id);  // "name"이 아닌 "user.id"가 나올 수 있음
// ✅ 해결: 한 단계로 문자열화
#define STRINGIFY(x) #x
#define FIELD(name) STRINGIFY(name)

에러 5: std::tuple as_tuple과 멤버 순서 불일치

원인: std::tie로 묶을 때 멤버 순서를 잘못 지정하면 잘못된 값이 직렬화됩니다.

// ❌ 잘못된 예
auto as_tuple(User& u) {
    return std::tie(u.name, u.id, u.email);  // 순서가 선언과 다름
}
// ✅ 해결: 구조체 선언 순서와 동일하게
auto as_tuple(User& u) {
    return std::tie(u.id, u.name, u.email);
}

에러 6: magic_enum enum_cast 실패 시 빈 optional

원인: 잘못된 문자열을 넘기면 std::nullopt 반환. 체크 없이 사용 시 크래시.

// ❌ 잘못된 예
auto status = magic_enum::enum_cast<Status>("Running");
do_something(status.value());  // "Runnig" 오타 시 nullopt → 예외
// ✅ 해결: has_value() 확인
auto status = magic_enum::enum_cast<Status>(input);
if (status.has_value()) {
    do_something(status.value());
} else {
    return error("Invalid status: " + input);
}

에러 7: 리플렉션으로 private 멤버 접근

원인: 멤버를 나열할 때 쓴 access_context와 실제 접근 위치의 권한이 맞지 않는 경우입니다. access_context::unchecked()로 private 멤버까지 나열한 뒤 obj.[:m:]로 읽는 것은 표준이 허용하는 사용법이지만, 그만큼 캡슐화를 우회한다는 뜻이라 라이브러리의 직렬화 계층 같은 곳에만 쓰는 것이 좋습니다.

// ✅ 일반 코드: 이 위치에서 접근 가능한 멤버만 (private 제외)
constexpr auto ctx = std::meta::access_context::current();
template for (constexpr auto m :
              std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
    use(obj.[:m:]);
}

에러 8: 이름을 std::string으로 받으려다 실패

원인: std::meta::identifier_of는 컴파일 타임에 만들어진 문자열을 가리키는 std::string_view를 돌려줍니다. 런타임 문자열이 필요하면 std::string(std::meta::identifier_of(m))처럼 필요한 곳에서 복사합니다. 이름이 없는 엔티티(익명 멤버 등)에 호출하면 상수 평가가 실패하므로 std::meta::has_identifier로 먼저 확인합니다.

constexpr std::string_view name = std::meta::identifier_of(^^Point);   // "Point"

컴파일러별 폴백과 멤버 목록을 한 곳에 두는 원칙

환경별 폴백 전략

#if defined(__cpp_impl_reflection)
    #define USE_CPP26_REFLECTION 1
#else
    #define USE_CPP26_REFLECTION 0
#endif
#if USE_CPP26_REFLECTION
    template <typename T>
    std::string to_json(const T& obj) {
        // C++26 리플렉션 기반
    }
#else
    template <typename T>
    std::string to_json(const T& obj) {
        // magic_enum + 수동 특수화
    }
#endif

enum은 magic_enum 사용

enum만 리플렉션이 필요하면 magic_enum이 가장 실용적입니다. 매크로 없이, 헤더 하나로 enum ↔ 문자열 변환이 가능합니다.

// 프로젝트에 magic_enum 추가
// vcpkg: vcpkg install magic-enum
// CMake: find_package(magic_enum CONFIG REQUIRED)

구조체 멤버는 X-Macro 한 곳 관리

멤버 목록을 한 곳에서 정의하며, 직렬화·검증·스키마 생성 등에서 재사용합니다.

// config_def.h
#define CONFIG_FIELDS \
    FIELD(int, port, 8080) \
    FIELD(std::string, host, "localhost") \
    FIELD(bool, ssl, false)

타입별 특수화로 복잡도 분리

범용 리플렉션이 없을 때, 타입별 특수화로 직렬화 로직을 분리합니다.

template <typename T>
struct Serializer;
template <>
struct Serializer<User> {
    static std::string to_json(const User& u) { /* ... */ }
    static User from_json(const json& j) { /* ... */ }
};

static_assert로 멤버 개수 검증

매크로나 수동 등록 시 멤버 개수를 static_assert로 검증해 누락을 방지합니다.

static_assert(sizeof(User) / sizeof(void*) >= 3, "User must have 3+ members");
// 또는
static_assert(std::tuple_size_v<decltype(as_tuple(std::declval<User>()))> == 3,
              "User must have exactly 3 members");

문서화

리플렉션/매크로 사용 시 동작 방식과 제한을 주석으로 명시합니다.

// REFLECT_STRUCT: USER_MEMBERS 매크로에 정의된 멤버만 직렬화됩니다.
// 멤버 추가 시 USER_MEMBERS와 to_json/from_json을 함께 수정해야 함.

magic_enum include 순서

MAGIC_ENUM_RANGE_*는 반드시 magic_enum.hpp include 이전에 정의합니다.

#define MAGIC_ENUM_RANGE_MIN 0
#define MAGIC_ENUM_RANGE_MAX 512
#include <magic_enum.hpp>

X-Macro 파일 분리

*_MEMBERS 정의는 별도 헤더(user_def.h)에 두며, 구조체·직렬화·스키마에서 include하여 재사용합니다.

enum 값 연속성

magic_enum은 0부터 연속된 enum에서 가장 안정적입니다. 비연속 값은 MAGIC_ENUM_RANGE_MAX 설정이 필요합니다.


설정 로드·프로토콜 직렬화·CLI 파싱에 enum 리플렉션 쓰기

패턴 1: 설정 로드 (magic_enum + 키 매핑)

#include <magic_enum.hpp>
#include <unordered_map>
#include <string>
enum class LogLevel { Debug, Info, Warning, Error };
struct Config {
    LogLevel level;
    std::string path;
};
Config load_config(const std::unordered_map<std::string, std::string>& kv) {
    Config c;
    if (auto it = kv.find("level"); it != kv.end()) {
        auto value = magic_enum::enum_cast<LogLevel>(it->second);
        if (value.has_value())
            c.level = value.value();
    }
    if (auto it = kv.find("path"); it != kv.end())
        c.path = it->second;
    return c;
}

패턴 2: 프로토콜 enum 직렬화

#include <magic_enum.hpp>
#include <cstdint>
enum class MsgType : uint8_t { Hello = 1, Data = 2, Bye = 3 };
uint8_t to_wire(MsgType t) {
    return static_cast<uint8_t>(t);
}
std::optional<MsgType> from_wire(uint8_t v) {
    return magic_enum::enum_cast<MsgType>(v);
}
std::string debug_name(MsgType t) {
    return std::string(magic_enum::enum_name(t));
}

패턴 3: 에러 코드 메시지

#include <magic_enum.hpp>
#include <iostream>
enum class ErrorCode { Success, NotFound, Timeout, Invalid };
const char* error_message(ErrorCode e) {
    return magic_enum::enum_name(e).data();
}
int main() {
    std::cerr << error_message(ErrorCode::Timeout) << "\n";
}

패턴 4: JSON 직렬화 (nlohmann/json + magic_enum)

#include <magic_enum.hpp>
#include <nlohmann/json.hpp>
enum class Status { Idle, Running, Stopped };
void to_json(nlohmann::json& j, Status s) {
    j = std::string(magic_enum::enum_name(s));
}
void from_json(const nlohmann::json& j, Status& s) {
    auto value = magic_enum::enum_cast<Status>(j.get<std::string>());
    if (value.has_value())
        s = value.value();
    else
        throw std::runtime_error("Invalid Status");
}

패턴 5: CLI 인자 파싱 (enum 옵션)

#include <magic_enum.hpp>
#include <iostream>
enum class OutputFormat { Json, Xml, Csv };
int main(int argc, char** argv) {
    if (argc < 2) return 1;
    auto format = magic_enum::enum_cast<OutputFormat>(argv[1]);
    if (!format.has_value()) {
        std::cerr << "Valid options: ";
        for (auto [v, n] : magic_enum::enum_entries<OutputFormat>())
            std::cerr << n << " ";
        return 1;
    }
    // ...
}

패턴 6: 단위 테스트 enum 커버리지

#include <magic_enum.hpp>
#include <gtest/gtest.h>
enum class State { A, B, C };
TEST(State, AllValuesHandled) {
    for (auto [value, name] : magic_enum::enum_entries<State>()) {
        switch (value) {
            case State::A: /* ... */ break;
            case State::B: /* ... */ break;
            case State::C: /* ... */ break;
        }
    }
}

패턴 7: X-Macro 기반 설정 로드

#define APP_CONFIG_FIELDS X(int, port, 8080) X(std::string, host, "127.0.0.1") X(bool, debug, false)
struct AppConfig {
#define X(a, b, c) a b;
    APP_CONFIG_FIELDS
#undef X
};
AppConfig load_from_env() {
    AppConfig c{8080, "127.0.0.1", false};
#define X(type, name, def) if (const char* v = getenv(#name)) { /* type별 파싱 */ }
    APP_CONFIG_FIELDS
#undef X
    return c;
}

패턴 8: 상태 머신 enum 디버그 출력

#include <magic_enum.hpp>
enum class FsmState { Init, Connecting, Connected, Closed };
void transition(FsmState from, FsmState to) {
    std::cout << magic_enum::enum_name(from) << " -> " << magic_enum::enum_name(to) << "\n";
}

패턴 9: enum 기반 메시지 라우팅

#include <magic_enum.hpp>
#include <unordered_map>
#include <functional>
enum class MsgType { Request, Response, Event, Error };
std::unordered_map<MsgType, std::function<void(const std::string&)>> handlers;
void route(const std::string& type_str, const std::string& payload) {
    auto type = magic_enum::enum_cast<MsgType>(type_str);
    if (type && handlers.count(*type)) handlers[*type](payload);
}

magic_enum·매크로·C++26 리플렉션의 컴파일·런타임 비용

magic_enum

  • enum_name: 컴파일 타임에 문자열 리터럴로 변환되므로 런타임 비용 거의 없음
  • enum_cast: 문자열 비교이므로 O(n), n은 enum 값 개수. 대부분 10~100 수준이라 무시 가능
  • enum_entries: 컴파일 타임에 배열 생성, 런타임에는 메모리 접근만

매크로 기반 직렬화

  • 직렬화: 멤버별로 직접 접근하므로 O(멤버 수), 인라인 가능
  • 매크로 확장: 컴파일 타임에만 발생, 런타임 영향 없음

C++26 리플렉션

  • 모든 메타 정보: 컴파일 타임에 결정
  • 스플라이싱: 컴파일 타임에 코드 생성됨
  • 런타임 오버헤드: 거의 없음 (타입 정보가 이미 코드에 포함)

비교표

방식컴파일 시간런타임 오버헤드바이너리 크기
magic_enum보통매우 낮음약간 증가
매크로낮음없음최소
C++26 리플렉션증가 가능없음보통
수동 등록낮음std::function 호출증가

방식별 요약

항목내용
C++26 Reflection^^ 연산자, std::meta::info, 스플라이싱 [: :]
magic_enumenum ↔ 문자열, enum 순회, C++17 이상
매크로X-Macro, REFLECT 매크로로 멤버 순회
직렬화C++26 또는 magic_enum + 수동 특수화
검증필수 필드 어노테이션, enum_cast 체크
에러magic_enum 범위, 익명 네임스페이스, 순서 불일치
프로덕션설정 로드, 프로토콜 직렬화, CLI 파싱

magic_enum·매크로 방식으로 갈 때 확인할 것

  • C++26 미지원 시 magic_enum 또는 매크로 사용
  • enum에 magic_enum 적용 시 MAGIC_ENUM_RANGE_MIN/MAX 확인
  • enum을 익명 네임스페이스에 두지 않기
  • enum_cast 실패 시 has_value() 체크
  • 매크로 사용 시 멤버 목록 한 곳에서 관리
  • static_assert로 멤버 개수·타입 검증
  • 프로덕션에서 타입별 폴백 전략 문서화

자주 묻는 질문 (FAQ)

Q. C++26 리플렉션은 언제 쓸 수 있나요?

A. 2025년 6월 C++26에 채택되었습니다. 2026년 기준 GCC 16이 -std=c++26 -freflection으로 지원하고, Clang은 Bloomberg의 clang-p2996 포크를 Compiler Explorer에서 쓸 수 있습니다. MSVC는 아직 지원하지 않으므로, 여러 컴파일러를 지원해야 한다면 당분간 magic_enum·Boost.PFR·매크로를 기본으로 두고 기능 테스트 매크로로 C++26 경로를 추가하는 방식을 권합니다.

Q. magic_enum vs 수동 switch, 어느 쪽이 나을까요?

A. enum 값이 자주 추가·변경되면 magic_enum이 유지보수에 유리합니다. enum이 고정이고 2~3개면 수동 switch도 괜찮습니다.

Q. 구조체 멤버 리플렉션은 어떻게 하나요?

A. C++26 미지원 환경에서 집합체(aggregate) 구조체라면 Boost.PFR이 가장 가볍습니다. 등록 없이 필드를 순회하고, C++20과 Boost 1.84 이상에서는 boost::pfr::get_name<I, T>()로 필드 이름도 얻을 수 있습니다. 그 밖에는 X-Macro, std::tuple + std::apply, 코드 생성 도구, RTTR 같은 런타임 라이브러리가 선택지입니다.


C++26 리플렉션으로 타입·멤버를 컴파일 타임에 조회할 수 있습니다. C++26 미지원 환경에서는 magic_enum(enum)과 매크로(구조체)로 직렬화·검증을 자동화하세요. 이전 글: C++ 실전 가이드 #26-2: 컴파일 타임 프로그래밍 기법 다음 글: C++ 실전 가이드 #27-1: Boost 라이브러리 활용


같이 보면 좋은 글