C++26 리플렉션 기초 | ^^ 연산자·std::meta::info로 타입 정보 조회하기

C++26 리플렉션 기초 | ^^ 연산자·std::meta::info로 타입 정보 조회하기

이 글의 핵심

C++26 리플렉션 기초에 대한 실전 가이드입니다. ^^ 연산자·std::meta::info로 타입 정보 조회하기 등을 예제와 함께 상세히 설명합니다.

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

직렬화 코드가 구조체마다 폭발한다

User, Order, Product 같은 구조체가 늘어날 때마다 to_json, from_json손으로 작성해야 한다면, 멤버를 추가·삭제·이름 변경할 때마다 직렬화 코드도 함께 수정해야 합니다. 누락 시 런타임 버그로 이어집니다.

비유하면: 도서관에 책이 100권 있는데, C++에는 “책 제목·저자·위치”를 자동으로 조회하는 카탈로그가 없어서 책마다 직접 목록을 손으로 적어 두는 상황입니다.

flowchart LR
  subgraph problem["문제 상황"]
    P1[User 구조체] --> P2[to_json 수동 작성]
    P3[Order 구조체] --> P4[to_json 수동 작성]
    P5[Product 구조체] --> P6[to_json 수동 작성]
    P2 --> P7[멤버 추가 시 누락 위험]
    P4 --> P7
    P6 --> P7
  end

  subgraph solution["리플렉션 해결"]
    S1[^^ 연산자로 타입 반사] --> S2[멤버 메타데이터]
    S2 --> S3[자동 직렬화]
    S3 --> S4[선언만으로 처리]
  end

C++26 리플렉션^^ 연산자로 타입·멤버 정보를 컴파일 타임에 조회하고, std::meta::nonstatic_data_members_of로 멤버를 순회할 수 있게 합니다. Java의 Class, C#의 Type처럼 타입 메타데이터를 언어가 제공하는 것입니다.

이 글을 읽으면:

  • C++26 ^^ 연산자와 std::meta::info의 개념을 이해할 수 있습니다.
  • 완전한 리플렉션 예제(C++26 문법, C++20 워크어라운드)를 적용할 수 있습니다.
  • 자주 발생하는 오류와 모범 사례를 익힐 수 있습니다.
  • 프로덕션에서 활용할 수 있는 패턴을 알 수 있습니다.

목차

  1. 문제 시나리오
  2. 리플렉션이란?
  3. C++26 ^^ 연산자와 std::meta::info
  4. 완전한 C++26 리플렉션 예제
  5. C++20 워크어라운드
  6. 자주 발생하는 오류
  7. 모범 사례 (Best Practices)
  8. 프로덕션 패턴
  9. 성능 고려사항
  10. 실전 주의사항
  11. 정리 및 다음 단계

1. 문제 시나리오

시나리오 1: JSON 직렬화 반복 코드

구조체가 10개, 100개로 늘어나면 to_json/from_json을 각각 작성하는 것은 유지보수 불가에 가깝습니다.

// ❌ 수동 직렬화: 멤버 추가 시마다 수정 필요
struct User {
    int id;
    std::string name;
    std::string email;
};

std::string to_json(const User& u) {
    return "{\"id\":" + std::to_string(u.id) +
           ",\"name\":\"" + u.name + "\"" +
           ",\"email\":\"" + u.email + "\"}";
}
// name을 username으로 바꾸면? age를 추가하면? 모두 수동 수정

시나리오 2: 게임 에디터 프로퍼티 패널

Transform, RigidBody, Mesh 등 컴포넌트의 멤버를 에디터 UI에 자동으로 노출하려면, 각 타입의 프로퍼티 이름·타입·범위를 런타임/컴파일 타임에 조회해야 합니다. 리플렉션이 없으면 컴포넌트마다 registerProperties() 같은 수동 등록 코드를 반복 작성해야 합니다.

시나리오 3: 네트워크 프로토콜 직렬화

서버-클라이언트 간 패킷 구조체가 50개 이상이면 수동 직렬화는 유지보수 불가입니다. 리플렉션으로 “멤버 순회 → 직렬화”를 공통화하면 새 패킷 추가 시 선언만 하면 됩니다.

시나리오 4: ORM/데이터베이스 매핑

User, Article 같은 엔티티를 DB 테이블과 매핑할 때, 컬럼 이름·타입을 알아야 쿼리 생성·결과 바인딩이 가능합니다. 리플렉션이 있으면 엔티티 클래스 선언만으로 매핑을 자동화할 수 있습니다.

시나리오 5: 설정 파일 로드

YAML/JSON 설정을 Config 구조체로 로드할 때, 키 이름과 멤버를 매칭하려면 멤버 이름을 알아야 합니다. 리플렉션으로 “키 → 멤버” 매핑을 자동화할 수 있습니다.

시나리오 6: CLI 인자 파싱

--name, --count 같은 옵션을 구조체 멤버와 자동으로 연결하고 싶을 때, 멤버 이름·타입·기본값을 조회해야 합니다. C++26 리플렉션과 어노테이션으로 clap 같은 라이브러리가 이를 구현합니다.

시나리오 7: 유닛 테스트 데이터 생성

테스트에서 User, Order 같은 객체를 여러 개 만들어야 할 때, 멤버를 순회하며 랜덤 값이나 픽스처를 채우는 코드가 반복됩니다. 리플렉션이 있으면 “멤버 타입별 기본값 주입”을 공통화할 수 있습니다. 타입이 50개면 50개의 createTest* 함수를 수동 작성해야 하는 문제가 있습니다.

시나리오 8: 로깅·디버그 출력

디버깅 시 객체의 모든 멤버를 "id=1, name=Alice, email=..." 형태로 출력하고 싶을 때, operator<<를 타입마다 수동 작성하면 유지보수 부담이 큽니다. 리플렉션으로 멤버를 순회하며 자동 출력할 수 있습니다.

시나리오 9: 설정 검증 (스키마 기반)

YAML/JSON 설정을 로드한 뒤 “필수 필드 존재 여부”, “타입 일치 여부”를 검증하려면, 구조체의 멤버 목록과 타입 정보가 필요합니다. 리플렉션으로 “선언된 멤버 = 기대 스키마”를 자동으로 만들어 검증 로직을 공통화할 수 있습니다.


2. 리플렉션이란?

기본 개념

리플렉션(Reflection)은 프로그램이 실행 중 또는 컴파일 타임에 자신의 타입·멤버·메서드 정보를 조회·순회할 수 있는 기능입니다.

구분런타임 리플렉션컴파일 타임 리플렉션 (C++26)
시점실행 중컴파일 시
정보타입 이름, 멤버 목록, 값 접근타입, 멤버, 이름 등
C++ 현재수동 등록, RTTR 등템플릿, constexpr, 매크로
C++26-^^ 연산자, std::meta::*
용도직렬화, UI 바인딩코드 생성, 직렬화 템플릿

C++26 정적 리플렉션의 특징

  • 컴파일 타임 전용: ^^ 연산자는 consteval/constexpr 맥락에서만 사용
  • 타입 안전: std::meta::info는 불투명 타입으로, 잘못된 사용 시 컴파일 에러
  • 표준화: P2996이 C++26에 반영됨 (2025년 6월 표준화 위원회 투표)
flowchart TB
  subgraph cpp20["C++20 이전"]
    A1[구조체 정의] --> A2[수동 등록 또는 매크로]
    A2 --> A3[타입 레지스트리]
    A3 --> A4[런타임 조회]
  end

  subgraph cpp26["C++26"]
    B1[구조체 정의] --> B2[^^ 연산자]
    B2 --> B3["std meta nonstatic_data_mem..."]
    B3 --> B4[컴파일 타임 자동 메타데이터]
  end

3. C++26 ^^ 연산자와 std::meta::info

^^ 연산자

^^ 연산자는 C++26의 리플렉션 연산자입니다. 원래 ^ 문법이 제안되었으나 Clang의 Objective-C++ 블록 확장과 충돌해 ^^로 변경되었습니다.

#include <meta>

int i;
consteval std::meta::info i_info = ^^i;  // i에 대한 메타데이터
  • ^^어떤 엔티티(변수, 타입, 멤버, 함수 등)에 적용하면 std::meta::info 값을 얻습니다.
  • std::meta::info불투명(opaque) 타입으로, 각 엔티티마다 고유한 값을 가집니다.
  • std::meta::info를 담는 변수는 반드시 consteval 또는 constexpr로 한정해야 합니다.

std::meta::info 비교

같은 엔티티에서 얻은 std::meta::info는 같고, 다른 스코프의 같은 이름은 다릅니다.

#include <meta>
#include <iostream>

struct S {};

int main() {
    S s{};
    consteval std::meta::info outer_s = ^^s;
    {
        S s{};
        consteval std::meta::info inner_s = ^^s;
        constexpr auto inner_s_copy = inner_s;

        constexpr bool outer_is_not_inner = (outer_s != inner_s);  // true
        constexpr bool inner_is_inner = (inner_s == inner_s_copy);  // true
        std::cout << std::boolalpha
                  << outer_is_not_inner << ' '
                  << inner_is_inner << '\n';
    }
}
// 출력: true true

인트로스펙션 함수

std::meta:: 네임스페이스의 consteval 함수들로 반사된 정보를 조회합니다.

#include <meta>

struct Point {
    int x;
    int y;
};

// 타입/멤버 이름 조회
static_assert(std::meta::name_v<^^Point> == "Point");
static_assert(std::meta::name_v<^^Point::x> == "x");

// 멤버 타입 조회
constexpr std::meta::info type_of_x = std::meta::type_of<^^Point::x>;
static_assert(std::meta::name_v<type_of_x> == "int");

멤버 순회

std::meta::nonstatic_data_members_of는 데이터 멤버들의 std::meta::info_range를 반환합니다.

#include <meta>
#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    for (constexpr std::meta::info member : std::meta::nonstatic_data_members_of<^^Point>) {
        std::cout << std::meta::name_v<member> << '\n';
    }
}
// 출력: x\n y

스플라이싱 (Splicing) 문법

[: ... :] 문법으로 반사된 타입 정보를 사용해 새 선언을 생성하거나 멤버에 접근합니다. 컴파일 타임에 std::meta::info 값을 “코드 조각”으로 치환하는 역할을 합니다.

#include <meta>
#include <iostream>

struct Point {
    int x;
    int y;
};

// 1. 타입 스플라이싱: type_of_x는 int 타입의 반사
constexpr std::meta::info type_of_x = std::meta::type_of<^^Point::x>;
[:type_of_x:] new_variable;  // int new_variable; 선언과 동일

// 2. 멤버 접근 스플라이싱: member_y는 Point::y의 반사
Point p{24, 42};
constexpr std::meta::info member_y = std::meta::nonstatic_data_members_of<^^Point>[1];
std::cout << p.[:member_y:] << '\n';  // 42 출력 (p.y와 동일)

// 3. 멤버 포인터 스플라이싱
constexpr std::meta::info member_x = std::meta::nonstatic_data_members_of<^^Point>[0];
int Point::* ptr = &Point::[:member_x:];  // &Point::x와 동일

스플라이싱 사용처: [:info:] (변수 선언) → 반사된 타입으로 선언; obj.[:member:] → 멤버 접근; &T::[:member:] → 멤버 포인터.

기능 테스트 매크로

#if __cpp_impl_reflection >= 202411L
// C++26 리플렉션 지원
#include <meta>
#endif

#if __cpp_lib_reflection >= 202411L
// 표준 라이브러리 리플렉션 지원
#endif

4. 완전한 C++26 리플렉션 예제

예제 1: 자동 JSON 직렬화 (기본)

#include <meta>
#include <string>
#include <sstream>

struct User {
    int id;
    std::string name;
    std::string email;
};

template <typename T>
std::string to_json_reflection(const T& obj) {
    std::ostringstream oss;
    oss << "{";
    bool first = true;
    for (constexpr std::meta::info member : std::meta::nonstatic_data_members_of<^^T>) {
        if (!first) oss << ",";
        oss << "\"" << std::meta::name_v<member> << "\":";
        // 타입별 직렬화 (실제 구현은 타입 분기 필요)
        first = false;
    }
    oss << "}";
    return oss.str();
}

// 사용
User u{1, "Alice", "[email protected]"};
auto json = to_json_reflection(u);  // {"id":1,"name":"Alice","email":"[email protected]"}

주의: 실제 구현에서는 std::meta::type_of<member>로 멤버 타입을 조회하고, int, std::string 등 타입별로 직렬화 로직을 분기해야 합니다. P3294(코드 주입)가 도입되면 더 일반적인 구현이 가능합니다.

예제 1-2: 타입별 직렬화 분기 (개념)

std::meta::type_of로 멤버 타입을 조회하고, std::meta::reflects_same으로 ^^int, ^^std::string 등과 비교해 타입별 직렬화를 분기할 수 있습니다. 한계: P3294(코드 주입) 전에는 임의의 std::meta::info에 대해 obj.[:member:]를 호출하는 완전히 일반적인 코드 작성이 어렵습니다. 타입별 if constexpr 분기나 도우미 템플릿이 필요합니다.

예제 1-3: ^^ 연산자로 타입·변수·멤버 반사

#include <meta>
#include <iostream>

struct Point { int x; int y; };

int global_var = 42;

int main() {
    // 1. 타입 반사
    constexpr std::meta::info point_info = ^^Point;
    static_assert(std::meta::name_v<point_info> == "Point");

    // 2. 변수 반사 (consteval 맥락 필요)
    consteval std::meta::info var_info = ^^global_var;
    static_assert(std::meta::name_v<var_info> == "global_var");

    // 3. 멤버 반사
    constexpr std::meta::info x_info = ^^Point::x;
    static_assert(std::meta::name_v<x_info> == "x");
    static_assert(std::meta::name_v<std::meta::type_of<x_info>> == "int");

    // 4. nonstatic_data_members_of로 순회
    constexpr auto members = std::meta::nonstatic_data_members_of<^^Point>;
    static_assert(std::meta::name_v<members[0]> == "x");
    static_assert(std::meta::name_v<members[1]> == "y");
}

예제 2: 멤버 이름 출력

#include <meta>
#include <iostream>

struct Config {
    std::string title;
    int width;
    int height;
    bool fullscreen;
};

template <typename T>
void print_member_names() {
    std::cout << "Members of " << std::meta::name_v<^^T> << ":\n";
    for (constexpr std::meta::info m : std::meta::nonstatic_data_members_of<^^T>) {
        std::cout << "  - " << std::meta::name_v<m> << " ("
                  << std::meta::name_v<std::meta::type_of<m>> << ")\n";
    }
}

int main() {
    print_member_names<Config>();
}

예제 3: 어노테이션과 함께 사용 (clap 스타일)

#include <meta>

enum class HashNotes { ignore };

struct Ultra {
    float data[3];
    [[HashNotes::ignore]] Cache cache;  // 해시 시 무시
};

// 제네릭 해시 함수 내부
// if (annotation_of_type<HashNotes>(data_member) != HashNotes::ignore) {
//     // obj.[:data_member:] 포함
// }

예제 4: CLI 인자 파싱 (개념)

// 사용자 정의 (clap 라이브러리 스타일)
struct Args : clap::Clap {
    [[Help("Name to greet")]]
    [[Short, Long]]
    std::string name;

    [[Help("Number of times to greet")]]
    [[Long("repeat")]]
    int count = 1;
};

int main(int argc, char** argv) {
    Args args;
    args.parse(argc, argv);  // 리플렉션으로 멤버 순회, 어노테이션으로 옵션 설정

    for (int i = 0; i < args.count; ++i) {
        std::cout << "Hello " << args.name << "!\n";
    }
}

5. C++20 워크어라운드

C++26 리플렉션이 없을 때는 수동 등록, 매크로, RTTR 등으로 대체합니다.

워크어라운드 1: 수동 등록

타입별로 프로퍼티를 수동 등록하고, 문자열 이름으로 접근하는 레지스트리를 만듭니다.

// reflection_core.h
#pragma once

#include <functional>
#include <string>
#include <unordered_map>
#include <vector>

using Getter = std::function<void(const void* obj, void* out)>;
using Setter = std::function<void(void* obj, const void* value)>;

struct PropertyInfo {
    std::string name;
    std::string typeName;
    Getter getter;
    Setter setter;
};

struct TypeInfo {
    std::string name;
    std::vector<PropertyInfo> properties;

    const PropertyInfo* findProperty(const std::string& name) const {
        for (const auto& p : properties) {
            if (p.name == name) return &p;
        }
        return nullptr;
    }
};

class TypeRegistry {
public:
    static TypeRegistry& instance() {
        static TypeRegistry reg;
        return reg;
    }
    void registerType(const std::string& name, TypeInfo info) {
        types_[name] = std::move(info);
    }
    const TypeInfo* getType(const std::string& name) const {
        auto it = types_.find(name);
        return it != types_.end() ? &it->second : nullptr;
    }
private:
    std::unordered_map<std::string, TypeInfo> types_;
};
// user_reflection.cpp — 등록은 cpp에서 (헤더 오염 방지)
#include "user.h"
#include "reflection_core.h"

void registerUserType() {
    TypeInfo info;
    info.name = "User";

    info.properties.push_back({
        "id", "int",
         {
            *static_cast<int*>(out) = static_cast<const User*>(obj)->id;
        },
         {
            static_cast<User*>(obj)->id = *static_cast<const int*>(value);
        }
    });

    info.properties.push_back({
        "name", "std::string",
         {
            new (out) std::string(static_cast<const User*>(obj)->name);
        },
         {
            static_cast<User*>(obj)->name = *static_cast<const std::string*>(value);
        }
    });

    TypeRegistry::instance().registerType("User", std::move(info));
}

워크어라운드 2: 매크로 기반 반복 제거

// reflection_macro.h
#define REFLECT_TYPE(TypeName) \
    void __register_##TypeName(); \

#define REFLECT_PROPERTY(Type, Member) \
    info.properties.push_back({ \
        #Member, #Type, \
         { \
            *static_cast<decltype(std::declval<Type>().Member)*>(out) = \
                static_cast<const Type*>(o)->Member; \
        }, \
         { \
            static_cast<Type*>(o)->Member = \
                *static_cast<const decltype(std::declval<Type>().Member)*>(v); \
        } \
    });

// 사용
REFLECT_TYPE(User)
void __register_User() {
    TypeInfo info;
    info.name = "User";
    REFLECT_PROPERTY(User, id);
    REFLECT_PROPERTY(User, name);
    REFLECT_PROPERTY(User, email);
    TypeRegistry::instance().registerType("User", std::move(info));
}

워크어라운드 3: RTTR 라이브러리

// user.h
struct User {
    int id;
    std::string name;
    std::string email;
};
// user_rttr.cpp
#include "user.h"
#include <rttr/registration>

RTTR_REGISTRATION {
    rttr::registration::class_<User>("User")
        .property("id", &User::id)
        .property("name", &User::name)
        .property("email", &User::email)
        .constructor<>();
}
// main.cpp
#include "user.h"
#include <rttr/registration>
#include <rttr/type>
#include <iostream>

int main() {
    User user{1, "Alice", "[email protected]"};
    auto type = rttr::type::get<User>();

    for (auto& prop : type.get_properties()) {
        std::cout << prop.get_name() << " = " << prop.get_value(user).to_string() << '\n';
    }
    return 0;
}

워크어라운드 4: Boost.PFR (C++14/17)

#include <boost/pfr.hpp>
#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    Point p{10, 20};
    std::cout << boost::pfr::get<0>(p) << ", " << boost::pfr::get<1>(p) << '\n';
    // 멤버 수: boost::pfr::tuple_size_v<Point>
}

한계: Boost.PFR은 aggregate 타입만 지원하고, 멤버 이름은 런타임에 얻을 수 없습니다. 인덱스 기반 접근만 가능합니다.

C++26 vs C++20 방식 비교

방식장점단점
C++26 ^^표준, 선언적, 컴파일 타임아직 컴파일러 지원 제한적
수동 등록명확, 의존성 없음반복 작업, 누락 위험
매크로반복 감소가독성 저하, 디버깅 어려움
RTTR기능 풍부, 검증됨외부 라이브러리 의존
Boost.PFR헤더만, 간단aggregate만, 이름 없음

6. 자주 발생하는 오류

오류 1: ^^ 연산자에 consteval/constexpr 없이 사용

원인: std::meta::info를 담는 변수는 반드시 consteval 또는 constexpr로 한정해야 합니다.

// ❌ 잘못된 예
std::meta::info i_info = ^^i;  // 컴파일 에러
// ✅ 올바른 예
consteval std::meta::info i_info = ^^i;
// 또는
constexpr auto i_info = ^^i;

오류 2: 멤버 추가 후 수동 등록 누락

원인: 수동 등록 시 새 멤버를 등록 목록에 추가하지 않음.

// ❌ User에 age 추가했지만 등록 누락
struct User {
    int id;
    std::string name;
    std::string email;
    int age;  // 새로 추가 — 등록 깜빡함
};
// age가 JSON에 안 나옴

해결법: 구조체 변경 시 등록 코드도 함께 수정하는 체크리스트를 두거나, 단위 테스트로 “등록된 프로퍼티 수 == 구조체 멤버 수”를 검증합니다.

오류 3: 비트리비얼 타입을 memcpy로 복사

원인: std::string 등은 placement new로 복사해야 합니다.

// ❌ 잘못된 예
p.getter =  {
    memcpy(out, &static_cast<const User*>(obj)->name, sizeof(std::string));
};  // 미정의 동작
// ✅ 올바른 예
p.getter =  {
    new (out) std::string(static_cast<const User*>(obj)->name);
};

오류 4: 인덱스로 프로퍼티 매칭 (순서 의존)

원인: JSON 키 순서와 등록 순서를 혼동.

// ❌ 잘못된 예 — 순서 바뀌면 잘못된 매핑
for (size_t i = 0; i < keys.size(); ++i) {
    type.properties[i].setter(obj, &values[i]);
}
// ✅ 올바른 예 — 이름으로 매칭
auto* prop = type.findProperty(key);
if (prop) prop->setter(obj, &value);

오류 5: ODR 위반 — 등록이 여러 번 실행됨

원인: 헤더에서 registerAllTypes()를 호출하고, 그 헤더를 여러 cpp에서 include하면 등록이 중복 실행될 수 있음.

// ❌ 잘못된 예 — header에서 호출
// reflection_init.h
registerUserType();   // 여러 TU에서 include 시 중복
// ✅ 올바른 예 — main 또는 단일 초기화 cpp에서만 호출
int main() {
    registerAllTypes();  // 한 곳에서만
    // ...
}

오류 6: 상속된 클래스의 베이스 멤버 누락

원인: 파생 클래스만 등록하고 베이스 멤버를 등록하지 않음.

// ❌ Derived만 등록 — id 조회 안 됨
struct Base { int id; };
struct Derived : Base { std::string name; };
// ✅ 베이스 멤버도 포함해 등록
void registerDerivedType() {
    TypeInfo info;
    info.name = "Derived";
    info.properties.push_back({"id", "int", ...});   // Base
    info.properties.push_back({"name", "std::string", ...});  // Derived
    // ...
}

오류 7: C++26 미지원 컴파일러에서 ^^ 사용

원인: Clang 실험적 브랜치, GCC 미지원 등.

해결법: #if __cpp_impl_reflection으로 분기하고, 미지원 시 C++20 워크어라운드로 폴백합니다.

#if __cpp_impl_reflection >= 202411L
    // C++26 리플렉션 경로
    return to_json_reflection<T>(obj);
#else
    // 수동 등록 또는 RTTR 경로
    return to_json_manual(obj);
#endif

오류 8: 스플라이싱에 런타임 변수 사용

원인: [: ... :] 안에는 constexpr/consteval로 평가되는 std::meta::info만 올 수 있습니다.

// ❌ 잘못된 예
[:get_member(0):] v;  // get_member(0)이 constexpr이 아니면 컴파일 에러

// ✅ 올바른 예
constexpr std::meta::info member_0 = std::meta::nonstatic_data_members_of<^^Point>[0];
[:member_0:] v;  // int v;

오류 9: ^^를 타입이 아닌 것에 적용

원인: ^^T에서 T타입이어야 합니다. template <int N>에서 ^^N은 에러(값은 반사 불가).

오류 10: 스플라이싱 위치 오류

원인: [:info:]는 선언 맥락에서 타입으로, 표현식 맥락에서 멤버 접근으로 해석됩니다. int x = [:type_of_x:];처럼 잘못된 위치에 쓰면 문법 오류가 납니다.

오류 11: aggregate가 아닌 타입에 Boost.PFR 사용

원인: Boost.PFR은 aggregate 타입만 지원합니다. 생성자, 기본값, private 비정적 데이터 멤버가 있으면 동작하지 않습니다.

// ❌ Boost.PFR 미지원
struct NonAggregate {
    int x;
    NonAggregate() : x(0) {}  // 사용자 정의 생성자
};
// boost::pfr::tuple_size_v<NonAggregate> — 컴파일 에러 또는 잘못된 결과
// ✅ aggregate만 사용
struct Aggregate {
    int x;
    int y;
};

7. 모범 사례 (Best Practices)

1. 등록은 cpp에서, 헤더 오염 최소화

리플렉션 등록 코드는 cpp 파일에 두고, 헤더에는 구조체 정의만 둡니다. 이렇게 하면 빌드 시간과 의존성이 줄어듭니다.

// user.h — 깔끔한 헤더
struct User {
    int id;
    std::string name;
    std::string email;
};
// user_reflection.cpp — 등록만
#include "user.h"
#include "reflection_core.h"
void registerUserType() { /* ... */ }

2. 비트리비얼 타입은 placement new

std::string, std::vector 등은 memcpy로 복사하면 안 됩니다. Getter에서 new (out) T(obj.member) 형태로 복사합니다.

3. 이름 기반 매칭, 인덱스 의존 금지

JSON 키, DB 컬럼 등은 이름으로 매칭합니다. 등록 순서에 의존하지 마세요.

4. 단일 진입점에서 등록

registerAllTypes()를 main 또는 앱 초기화에서 한 번만 호출합니다. ODR 위반을 방지합니다.

5. 단위 테스트로 등록 누락 검증

TEST(Reflection, UserPropertiesCount) {
    auto* type = TypeRegistry::instance().getType("User");
    ASSERT_NE(type, nullptr);
    ASSERT_EQ(type->properties.size(), 3u);  // id, name, email
}

6. C++26 사용 시 기능 테스트

#if __cpp_impl_reflection >= 202411L
    // 리플렉션 사용
#else
    #pragma message("Reflection not supported, using fallback")
    // 워크어라운드
#endif

7. private 멤버는 friend 또는 접근자 사용

리플렉션으로 private 멤버에 접근하려면, 등록 시 friend 함수 또는 public getter/setter를 통해 접근합니다.

class SecretData {
    int secret_;
public:
    int getSecret() const { return secret_; }
    void setSecret(int v) { secret_ = v; }
};

// 등록 시 getter/setter 연결
info.properties.push_back({
    "secret", "int",
     {
        *static_cast<int*>(out) = static_cast<const SecretData*>(o)->getSecret();
    },
     {
        static_cast<SecretData*>(o)->setSecret(*static_cast<const int*>(v));
    }
});

8. std::meta::info는 값으로 전달·비교

std::meta::info는 불투명 타입이지만 값 의미론을 가집니다. 복사해도 동일한 엔티티를 가리키며, ==/!=로 비교할 수 있습니다.

9. 리플렉션 결과 캐싱 (C++20 워크어라운드)

수동 등록 시 TypeInfo를 한 번만 구성하고, getType(name) 결과를 캐시해 두면 반복 조회 비용을 줄입니다. C++26 리플렉션은 컴파일 타임이므로 런타임 캐시가 필요 없습니다.

10. 타입 이름 문자열 일관성

typeName"int", "std::string"처럼 플랫폼·컴파일러에 독립적인 형식으로 통일합니다. typeid(T).name()은 구현체마다 다르므로 직렬화/역직렬화 시 문자열 매칭이 깨질 수 있습니다.


8. 프로덕션 패턴

패턴 1: 방문자 패턴으로 직렬화 포맷 분리

JSON, 바이너리, 프로토콜 버퍼 등 포맷이 다르면, 방문자 패턴으로 포맷별 직렬화를 분리합니다.

struct SerializationVisitor {
    virtual void visitInt(const std::string& name, int value) = 0;
    virtual void visitString(const std::string& name, const std::string& value) = 0;
};

void serializeWithVisitor(const void* obj, const TypeInfo& type, SerializationVisitor& v) {
    for (const auto& p : type.properties) {
        if (p.typeName == "int") {
            int val;
            p.getter(obj, &val);
            v.visitInt(p.name, val);
        } else if (p.typeName == "std::string") {
            std::string val;
            p.getter(obj, &val);
            v.visitString(p.name, val);
        }
    }
}

패턴 2: 메타데이터 확장 (UI 힌트, 검증 규칙)

프로퍼티에 “범위”, “에디터 타입” 등을 메타데이터로 붙입니다.

struct PropertyInfo {
    std::string name;
    std::string typeName;
    Getter getter;
    Setter setter;
    std::unordered_map<std::string, std::string> metadata;
};

// 등록 시
info.properties.push_back({
    "screenWidth", "int", getter, setter,
    {{"min", "0"}, {"max", "7680"}, {"editor", "slider"}}
});

패턴 3: 스키마 생성 (API 문서, 클라이언트 코드 생성)

리플렉션 정보로 JSON Schema, OpenAPI 스펙을 생성합니다.

std::string toJsonSchema(const TypeInfo& type) {
    nlohmann::json schema;
    schema["type"] = "object";
    schema["properties"] = nlohmann::json::object();
    for (const auto& p : type.properties) {
        schema["properties"][p.name]["type"] = typeNameToJson(p.typeName);
    }
    return schema.dump(2);
}

패턴 4: 버전별 직렬화 (이전 버전 호환)

멤버 이름 변경·삭제 시 이전 포맷과 호환하려면, 메타데이터에 alias 또는 sinceVersion을 둡니다.

info.metadata["alias"] = "old_id";  // 이전 필드 이름

패턴 5: 플러그인 동적 등록/해제

플러그인이 로드될 때 타입을 등록하고, 언로드 시 제거합니다.

void TypeRegistry::unregisterType(const std::string& name) {
    types_.erase(name);
}

void onPluginUnload(const std::string& pluginName) {
    for (const auto& name : getTypesFromPlugin(pluginName)) {
        TypeRegistry::instance().unregisterType(name);
    }
}

패턴 6: C++26/워크어라운드 이중 경로 (Feature Detection)

프로덕션에서는 C++26 리플렉션 지원 여부에 따라 자동으로 경로를 전환합니다.

template <typename T>
std::string to_json(const T& obj) {
#if __cpp_impl_reflection >= 202411L
    return to_json_reflection(obj);
#else
    return to_json_registry(obj);  // TypeRegistry 기반
#endif
}

패턴 7: 중첩 구조체 직렬화 (재귀 방문)

User 안에 Address가 있는 식의 중첩 구조체는, TypeInfo에 자식 타입 정보를 두고 serializeNested를 재귀 호출합니다. p.typeName == "Address"일 때 해당 프로퍼티의 getter로 주소를 추출한 뒤, AddressTypeInfo로 다시 직렬화합니다.

패턴 8: 리플렉션 기반 비교 연산자 생성

C++20 워크어라운드에서는 REFLECT_EQUAL 매크로로 operator==를 생성합니다. C++26에서는 nonstatic_data_members_of로 멤버를 순회하며 비교할 수 있으며, P3294(코드 주입) 도입 시 완전 자동 생성이 가능합니다.


9. 성능 고려사항

레지스트리 조회

getType(name)unordered_map이면 O(1)입니다. findProperty는 선형 검색이므로, 프로퍼티가 10개 이상이면 unordered_map으로 전환하는 것이 좋습니다.

Getter/Setter 오버헤드

std::function 호출은 가상 함수 호출 수준의 오버헤드가 있습니다. 직렬화가 병목이면, 타입별 전용 함수를 템플릿으로 두고 인라인 최적화를 노립니다.

C++26 리플렉션

컴파일 타임 리플렉션은 런타임 오버헤드가 없습니다. 모든 조회가 컴파일 시 수행되므로, 생성된 코드는 수동 직렬화와 동등한 성능을 낼 수 있습니다.

캐시된 TypeInfo

자주 직렬화되는 타입은 TypeInfo 포인터를 캐시해 두고, 매번 getType(name)을 호출하지 않습니다.

// 앱 초기화 시 한 번만
const TypeInfo* userType = TypeRegistry::instance().getType("User");

// 직렬화 루프에서
for (const auto& user : users) {
    toJsonReflection(&user, *userType);
}

10. 실전 주의사항

C++26 컴파일러 지원

2025년 기준 C++26 리플렉션은 Clang 실험적 브랜치에서만 부분 지원됩니다. Compiler Explorer에서 -std=c++2c와 Clang 최신 버전을 선택해 테스트할 수 있습니다. GCC, MSVC는 아직 공식 지원이 없을 수 있으므로, 프로덕션에서는 #if __cpp_impl_reflection으로 폴백 경로를 반드시 두세요.

스플라이싱 문법 제한

[: ... :] 스플라이싱은 선언멤버 접근에 사용됩니다. 복잡한 표현식 내부에서는 제한이 있을 수 있으므로, 제안서와 컴파일러 문서를 확인하세요.

템플릿 타입의 반사

std::vector<int>, std::map<std::string, int> 같은 템플릿 인스턴스도 ^^로 반사할 수 있습니다. 타입 이름이 길어지므로, 디버깅·로깅 시 std::meta::name_v 결과가 예상과 다를 수 있습니다.

디버깅 팁

1. 등록이 안 될 때
registerAllTypes()가 실제로 호출되는지 확인하세요. 정적 초기화 순서 문제로 레지스트리가 비어 있을 수 있습니다. main 시작 시 명시적으로 호출하는 것이 안전합니다.

2. 잘못된 타입으로 직렬화할 때
typeName 문자열 비교는 오타에 취약합니다. "std::string" vs "string" 같은 차이에 주의하고, 가능하면 타입 인덱스나 typeid를 활용하세요.

3. C++26 코드가 컴파일되지 않을 때
^^ 연산자 미지원 시 consteval 또는 constexpr 맥락인지 확인하세요. std::meta::info 변수에 이 한정자가 없으면 에러가 납니다.


11. 정리 및 다음 단계

핵심 요약

항목설명
^^ 연산자C++26 리플렉션 연산자, std::meta::info 반환
std::meta::info불투명 타입, consteval/constexpr 필수
nonstatic_data_members_of데이터 멤버 순회
스플라이싱 [: :]반사된 타입으로 선언 생성
C++20 워크어라운드수동 등록, 매크로, RTTR, Boost.PFR

적용 시나리오별 선택 가이드

시나리오권장 방식비고
C++26 지원 환경^^ 연산자, std::meta::*표준, 선언적
C++20, 의존성 최소수동 등록명확, 유지보수 부담
C++20, 반복 제거매크로가독성 trade-off
C++20, 기능 풍부RTTR외부 라이브러리
aggregate만, 인덱스 접근Boost.PFR이름 없음

구현 체크리스트

  • C++26 사용 시 __cpp_impl_reflection 확인
  • 등록은 cpp에서, 헤더 오염 방지
  • 비트리비얼 타입 placement new 처리
  • 프로퍼티 이름으로 매칭 (순서 의존 금지)
  • ODR 방지 — 등록 초기화는 단일 진입점
  • 상속 시 베이스 멤버도 등록
  • 단위 테스트로 등록 누락 검증

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 자동 직렬화, ORM 매핑, 게임 에디터 프로퍼티 바인딩, 설정 로드, 스크립트 바인딩 등에서 타입 구조를 자동으로 조회할 때 사용합니다. C++26 이전에는 수동 등록·매크로·RTTR 등으로 구현합니다.

Q. 선행으로 읽으면 좋은 글은?

A. 컴파일 타임 프로그래밍(#26-2), 가변 인자 템플릿(#09-3)을 먼저 읽으면 이해가 쉽습니다.

Q. 더 깊이 공부하려면?

A. P2996 Reflection 제안서, cppreference, Clang 실험적 리플렉션을 참고하세요.

한 줄 요약: C++26 ^^ 연산자와 std::meta::info로 타입·멤버 정보를 컴파일 타임에 조회할 수 있습니다. C++20에서는 수동 등록·매크로·RTTR으로 대체합니다. 다음으로 런타임 리플렉션 구현(#55-1)을 읽어보면 좋습니다.

참고 자료

다음 글: [C++ 실전 가이드 #26-2] 컴파일 타임 프로그래밍 기법: constexpr·consteval·템플릿 메타

이전 글: [C++ 실전 가이드 #25-3] 커스텀 Range 작성: range 개념을 만족하는 타입 만들기


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전
  • C++ 리플렉션 구현 | 타입 정보·메타데이터·자동 직렬화 [#55-1]
  • C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


이 글에서 다루는 키워드 (관련 검색어)

C++, C++26, 리플렉션, reflection, std::meta::info, ^^ 연산자, 메타프로그래밍, 직렬화 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 컴파일 타임 리플렉션 | C++26 Reflection·magic_enum·매크로 직렬화·검증
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]
  • C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전
  • C++ Type Traits 완벽 가이드 | std::is_integral·std::enable_if
  • C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기