C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]

들어가며: 직렬화 코드를 매번 손으로 작성해야 한다

C++에서 JSON 직렬화, ORM 바인딩, 디버그 출력, 명령줄 인자 파싱을 구현할 때마다 멤버 변수를 하나씩 나열하는 코드를 반복 작성한 경험이 있을 겁니다. Java나 C#에는 Reflection(리플렉션)이 있어서 타입 정보를 실행 시점에 조회할 수 있지만, C++는 전통적으로 컴파일 타임에 타입 정보가 사라지는 언어였습니다. 따라서 코드 생성기(protobuf, Qt MOC)나 매크로에 의존해 왔습니다. C++26을 목표로 Reflection(반사)이 표준에 들어가면서, 컴파일 타임에 타입·멤버·이름을 조회하고 코드 생성 없이 직렬화·바인딩·테스트를 자동화할 수 있는 가능성이 열립니다. 이 글에서는 Reflection 제안(P2996)과 std::execution(P2300) 등 C++26의 주요 기능을 문제 시나리오, 완전한 예시, 일반적인 에러, 마이그레이션 팁, 프로덕션 패턴과 함께 실전 관점에서 다룹니다. 비유: Reflection은 “건물의 설계도”를 런타임에 꺼내는 것과 같습니다. C++는 지금까지 설계도를 컴파일 후 버려서, 실행 시에 “이 구조체에 어떤 멤버가 있지?”를 알 수 없었습니다. C++26 Reflection은 컴파일 타임에 설계도를 읽어서, 직렬화·바인딩 같은 반복적인 코드를 자동으로 생성할 수 있게 합니다.

flowchart LR
  subgraph before[C++23 이전]
    B1[구조체 정의] --> B2[수동 to_json]
    B2 --> B3[수동 from_json]
    B3 --> B4[멤버 추가 시 수정 필요]
  end
  subgraph after[C++26 Reflection]
    A1[구조체 정의] --> A2[^^ 타입으로 반사]
    A2 --> A3[멤버 자동 순회]
    A3 --> A4[직렬화/바인딩 자동 생성]
  end

추가 문제 시나리오: API 응답 구조체 변경

REST API 응답을 담는 Response 구조체에 필드가 추가될 때마다 to_json, from_json, 단위 테스트의 예상값을 모두 수동으로 수정해야 합니다. Reflection이 있으면 구조체 정의만 바꿔도 직렬화·테스트가 자동으로 따라갑니다.

ORM 엔티티와 DB 컬럼 매핑

User 테이블과 User 엔티티를 매핑할 때, 컬럼 이름을 문자열로 하드코딩하면 오타가 나기 쉽으며, 리팩터링 시 누락됩니다. Reflection으로 멤버 이름을 컴파일 타임에 조회하면 타입 안전성이 확보됩니다.

비동기 체인 조합

std::async로 여러 비동기 작업을 연결하면 future를 중첩하며, 예외·취소 처리가 복잡해집니다. std::execution의 Sender/Receiver는 | 연산자로 선언적으로 체인을 만들고, 취소·에러 전파를 표준화합니다.

설정 파일 로딩

YAML/JSON 설정을 구조체로 파싱할 때, 필드마다 node[key]를 호출하는 반복 코드가 생깁니다. Reflection으로 멤버를 순회하면 설정 로더를 제네릭하게 만들 수 있습니다.

이 글에서 다루는 것:

  • Reflection 제안(P2996): ^^ 연산자, std::meta::info, splice 문법, 실전 예시
  • std::execution(P2300): Sender/Receiver 비동기 모델
  • 기타 C++26 기능: Contracts, std::simd, std::hive 등과 C++26에 들어가지 않은 것
  • 문제 시나리오, 일반적인 에러, 마이그레이션, 프로덕션 패턴

Reflection (P2996)

컴파일 타임 타입 정보

P2996 “Reflection for C++26”은 2025년 6월 WG21 회의에서 C++26에 채택되었습니다. 같은 회의에서 이를 실용적으로 만드는 제안들, 즉 확장 문 template for(P1306), std::define_static_array(P3491), 어노테이션 [[=...]](P3394), 접근 문맥 std::meta::access_context(P3547)도 함께 채택되었습니다. 이 글의 초기 버전은 표준화 도중의 문법(name_of, 인자 하나짜리 nonstatic_data_members_of, 일반 for 루프)으로 작성되어 있었는데, 채택된 형태에 맞춰 예제를 모두 고쳤습니다.

요소설명
^^ 연산자타입·변수·멤버·함수·네임스페이스·열거자에 적용하면 std::meta::info 값
std::meta::info반사된 엔티티를 나타내는 불투명 스칼라 타입. 컴파일 타임에만 존재
메타함수identifier_of, type_of, nonstatic_data_members_of(r, ctx), enumerators_of 등 consteval 함수
스플라이스 [: r :]info를 다시 코드(값·타입·멤버 접근)로
template for컴파일 타임 범위를 원소마다 펼치는 확장 문
어노테이션 [[=v]]선언에 상수 값을 붙이고 리플렉션으로 읽기

기본 문법

#include <meta>
#include <iostream>

struct Point { int x; int y; };

int main() {
    constexpr auto point_name = std::meta::identifier_of(^^Point);
    constexpr auto x_name = std::meta::identifier_of(^^Point::x);
    std::cout << point_name << ", " << x_name << '\n';   // Point, x
}
  • ^^Point: Point 타입을 반사한 std::meta::info 값
  • std::meta::identifier_of(r): 선언된 이름(std::string_view). 이름이 없는 엔티티면 상수 평가 실패 (has_identifier로 먼저 확인)
  • info를 담는 변수는 constexpr이어야 합니다. consteval은 함수에만 붙는 지정자라 변수 선언에는 쓸 수 없습니다.

멤버 순회

#include <meta>
#include <iostream>
#include <string>

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

template <typename T>
void print_member_names() {
    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))) {
        std::cout << std::meta::identifier_of(m) << '\n';
    }
}
// print_member_names<User>() → id, name, email
  • nonstatic_data_members_of(r, ctx): 비정적 데이터 멤버를 선언 순서대로 담은 std::vector<std::meta::info>. 두 번째 인자로 접근 문맥을 받습니다. current()는 호출 위치에서 접근 가능한 멤버만, unchecked()는 private까지 전부입니다.
  • 이 벡터는 컴파일 타임에만 존재할 수 있으므로 std::define_static_array로 정적 배열로 바꿔 template for에 넘깁니다. 일반 for로는 루프 변수가 상수가 아니라서 [: m :]에 쓸 수 없습니다.

Splice 문법

Point p{24, 42};
std::cout << p.[: ^^Point::y :] << '\n';                        // 42 (p.y와 동일)
typename [: std::meta::type_of(^^Point::x) :] v = 7;            // int v = 7;
int Point::* pm = &[: ^^Point::x :];                            // 멤버 포인터

주요 메타 함수 요약

메타 함수용도
identifier_of(r)선언된 이름 (식별자)
display_string_of(r)표시용 문자열 (구현마다 다를 수 있음, 로그용)
type_of(r)멤버·변수의 타입
nonstatic_data_members_of(r, ctx)비정적 데이터 멤버
members_of(r, ctx)모든 멤버 (함수 포함)
bases_of(r, ctx)기반 클래스
enumerators_of(r)열거자
is_public(r), is_private(r) 등성질 검사
annotations_of_with_type(r, ^^T)특정 타입의 어노테이션

Reflection 예시: 직렬화, CLI 옵션 어노테이션, 필드 검증

예시 1: 제네릭 직렬화 (디버그 출력)

#include <meta>
#include <iostream>
#include <sstream>
#include <string>
struct Config {
    int port;
    std::string host;
    bool debug;
};
template <typename T>
std::string to_string(const T& obj) {
    std::ostringstream oss;
    oss << std::boolalpha << "{";
    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) oss << ", ";
        oss << std::meta::identifier_of(m) << ": " << obj.[:m:];
        first = false;
    }
    oss << "}";
    return oss.str();
}
int main() {
    Config c{8080, "localhost", true};
    std::cout << to_string(c) << '\n';
    // {port: 8080, host: localhost, debug: true}
}

실전 활용: 설정 로깅, 디버그 덤프. 구조체에 멤버를 추가해도 to_string을 수정할 필요가 없습니다. 멤버에 operator<<가 없는 타입이 있으면 그 자리에서 컴파일 에러가 나므로, 실제로는 타입별 분기(아래 예시 5)를 둡니다.

예시 2: 어노테이션으로 CLI 옵션 선언

C++26 어노테이션은 [[=값]] 형태로 상수 값을 선언에 붙입니다. 일반 속성([[nodiscard]])과 달리 값을 리플렉션으로 읽을 수 있어서, Rust의 clap·serde 같은 선언적 설정을 표준 C++로 흉내 낼 수 있습니다.

#include <meta>
#include <string_view>

struct help  { std::string_view text; };
struct flag  { std::string_view long_name; char short_name = 0; };

struct Args {
    [[=help{"Name to greet"}, =flag{"name", 'n'}]] std::string name;
    [[=help{"Number of times to greet"}, =flag{"repeat"}]] int count = 1;
};

template <typename T>
void print_usage() {
    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))) {
        constexpr auto flags = std::define_static_array(std::meta::annotations_of_with_type(m, ^^flag));
        constexpr auto helps = std::define_static_array(std::meta::annotations_of_with_type(m, ^^help));
        if constexpr (flags.size() > 0 && helps.size() > 0) {
            constexpr flag f = std::meta::extract<flag>(flags[0]);
            constexpr help h = std::meta::extract<help>(helps[0]);
            std::cout << "  --" << f.long_name << "  " << h.text << '\n';
        }
    }
}

실전 활용: 사용법 출력과 인자 파싱을 같은 선언에서 만들어, 옵션을 추가할 때 파서·도움말·검증을 따로 고칠 필요가 없게 합니다. 실제 파싱 로직은 같은 루프에서 argv와 f.long_name을 비교해 obj.[:m:]에 값을 넣는 식으로 작성합니다.

예시 3: 해시 함수에서 제외할 멤버 (어노테이션)

#include <meta>
#include <functional>

struct hash_ignore_t {};
inline constexpr hash_ignore_t hash_ignore{};

struct Ultra {
    int id;
    float score;
    [[=hash_ignore]] std::string cache;   // 해시에서 제외
};

template <typename T>
std::size_t hash_value(const T& obj) {
    std::size_t h = 0;
    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, ^^hash_ignore_t).empty()) {
            using M = std::remove_cvref_t<decltype(obj.[:m:])>;
            h ^= std::hash<M>{}(obj.[:m:]) + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2);
        }
    }
    return h;
}

실전 활용: 캐시·임시 값처럼 동일성에 영향을 주지 않는 멤버를 표시하고, 해시·비교·직렬화에서 일관되게 제외합니다.

예시 4: C++23 이전 대비 (수동 vs Reflection)

// C++23 이전: 수동 작성, 멤버 추가 시 반드시 수정
std::string to_json_manual(const Config& c) {
    return "{\"port\":" + std::to_string(c.port) +
           ",\"host\":\"" + c.host + "\"}";
}
// C++26: 위 예시 1의 to_string처럼 멤버 목록을 순회 → 멤버 추가 시 수정 불필요

예시 5: 실전 — 필수 필드 검증

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

struct ServerConfig {
    [[=required]] std::string host;
    [[=required]] std::string cert_path;
    int timeout = 30;                // 선택
};

template <typename T>
std::vector<std::string> missing_fields(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;
}

예시 6: 실전 — 테스트 데이터 생성기

template <typename T>
T make_test_fixture() {
    T obj{};
    int idx = 0;
    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))) {
        using M = std::remove_cvref_t<decltype(obj.[:m:])>;
        if constexpr (std::is_same_v<M, std::string>)      obj.[:m:] = "test" + std::to_string(idx);
        else if constexpr (std::is_same_v<M, bool>)        obj.[:m:] = false;
        else if constexpr (std::is_arithmetic_v<M>)        obj.[:m:] = static_cast<M>(idx);
        ++idx;
    }
    return obj;
}
struct Order { int id; std::string product; double price; };
// make_test_fixture<Order>() → id=0, product="test1", price=2.0

std::execution (Sender/Receiver)

std::async의 한계

C++11의 std::async와 std::future는 조합이 어렵고, 취소 지원이 없으며, 실행 경로 제어가 불명확합니다. P2300 std::execution은 Sender/Receiver 모델로 비동기 작업을 조합 가능하게 만듭니다.

flowchart LR
  subgraph old[std async]
    O1[future] --> O2[get 블로킹]
    O2 --> O3[조합 어려움]
  end
  subgraph new[std execution]
    N1[Sender] --> N2[connect]
    N2 --> N3[Receiver]
    N3 --> N4[set_value/set_error/set_done]
    N4 --> N5[then, let_value 등으로 조합]
  end

핵심 개념

개념설명
Sender비동기 작업을 나타내는 객체. connect로 Receiver와 연결
Receiverset_value, set_error, set_done으로 완료 시그널 수신
Operation Stateconnect 결과. start()로 작업 시작
Scheduler작업을 어디서 실행할지 결정

기본 예시

// C++26 std::execution 예상 (P2300 채택)
#include <execution>
#include <iostream>
int main() {
    auto sched = std::execution::run_loop();
    auto sender = std::execution::schedule(sched.get_scheduler())
        | std::execution::then([] { std::cout << "Hello, async!\n"; });
    auto [op_state] = std::execution::connect(sender, std::execution::receiver());
    std::execution::start(op_state);
    sched.run();
    return 0;
}

코드 설명:

  • schedule: 스케줄러에서 실행할 Sender 생성
  • then: 이전 Sender의 결과를 받아 다음 작업 연결
  • connect + start: Receiver와 연결 후 실행 시작

실전: 비동기 체인

// then, let_value로 비동기 체인 구성
#include <execution>
auto async_work() {
    return std::execution::schedule(scheduler)
        | std::execution::then([] { return fetch_data(); })
        | std::execution::let_value([](auto data) {
            return process(data);
        })
        | std::execution::then([](auto result) {
            return save_result(result);
        });
}

실전 활용: 네트워크 요청 → 파싱 → DB 저장 같은 파이프라인을 | 연산자로 선언적으로 구성합니다.


기타 C++26 기능

Contracts (P2900)

함수의 전제조건·사후조건과 본문 안의 단언을 언어 문법으로 표현하는 기능이 2025년 2월 C++26에 채택되었습니다. 초기 제안에 있던 [[assert: ...]] 속성 형태가 아니라 문맥 키워드 형태입니다.

int safe_div(int a, int b)
    pre(b != 0)                       // 전제조건
    post(r: r * b <= a)               // 사후조건 (반환값에 이름 r)
{
    contract_assert(a >= 0);          // 본문 단언 (assert 매크로 대체)
    return a / b;
}

위반 시 동작은 코드가 아니라 빌드 설정(평가 의미론)으로 정합니다. 검사를 끄는 ignore, 위반을 보고만 하는 observe, 보고 후 종료하는 enforce, 바로 종료하는 quick_enforce 중에서 고르고, 위반 처리기를 교체해 로깅을 넣을 수도 있습니다. 매크로 assert와 달리 인터페이스에 붙어서 문서 역할을 하고, 도구가 읽을 수 있다는 것이 장점입니다.

표준 라이브러리

  • std::simd(P1928): 이식 가능한 SIMD 타입. GCC의 <experimental/simd>로 미리 써 볼 수 있습니다.
  • std::hive(P0447): 삭제해도 다른 원소의 반복자가 무효화되지 않는 블록 기반 컨테이너. 게임 엔티티처럼 삽입·삭제가 잦은 컬렉션용.
  • std::inplace_vector(P0843): 용량이 고정된, 동적 할당 없는 벡터.
  • <linalg>(P1673): BLAS 스타일 선형대수 알고리즘.
  • <debugging>(P2546): std::breakpoint(), std::is_debugger_present().
  • #embed(P1967): 바이너리 파일을 컴파일 타임에 배열로 포함.
  • 포화 산술(std::add_sat 등), std::text_encoding, constexpr 확대 등.

C++26에 들어가지 않은 것

네트워킹(std::net, Networking TS 계열)과 2D 그래픽 제안은 C++26에 포함되지 않았습니다. 네트워크는 std::execution 위에 새로 설계하는 방향으로 논의가 이어지고 있어, 당분간은 Boost.Asio나 standalone Asio를 쓰게 됩니다. 스플라이스를 템플릿 인자로 쓰는 일부 기능과 코드 주입(토큰 시퀀스, P3294) 같은 리플렉션 확장도 C++29 이후로 넘어갔습니다.


런타임 변수에 담은 info, ^^와 ^ 혼동, Receiver 연결 누락 같은 에러

에러 1: std::meta::info를 런타임 변수에 담음

증상: std::meta::info를 일반 변수에 담거나 런타임 함수 인자로 넘기면 컴파일 에러 원인: std::meta::info는 consteval-only 타입이라 컴파일 타임에만 존재합니다.

// ❌ 잘못된 코드
void bad() {
    std::meta::info i = ^^SomeType;          // 런타임 변수 불가
    consteval std::meta::info j = ^^SomeType; // consteval은 변수에 못 붙임
}
// ✅ 올바른 코드
void good() {
    constexpr std::meta::info i = ^^SomeType;
    constexpr auto name = std::meta::identifier_of(i);   // 결과(string_view)는 런타임에 써도 됨
}

에러 2: private 멤버가 목록에 없음

증상: nonstatic_data_members_of에 private 멤버가 나오지 않음 원인: 접근 문맥을 access_context::current()로 넘기면 호출 위치에서 접근 가능한 멤버만 나옵니다. 클래스 밖의 제네릭 함수에서는 private 멤버가 빠집니다.

// private까지 필요하면 의도를 명시
constexpr auto ctx = std::meta::access_context::unchecked();
auto all = std::meta::nonstatic_data_members_of(^^T, ctx);

unchecked()로 private 멤버를 읽는 것은 표준이 허용하지만 캡슐화를 우회하는 것이므로, 직렬화 라이브러리처럼 필요한 곳에만 쓰는 것이 좋습니다.

에러 3: ^^ 연산자와 ^ 혼동

증상: ^Point 사용 시 에러 원인: 초기 제안의 ^ 한 글자 문법은 Clang의 Objective-C 블록 확장과 충돌해 ^^로 바뀌었습니다.

// ❌ 잘못된 코드
constexpr auto info = ^Point;
// ✅ 올바른 코드
constexpr auto info = ^^Point;

에러 4: 일반 for로 멤버를 순회하며 스플라이스

증상: for (constexpr auto m : members) obj.[:m:]에서 “not a constant expression” 등의 에러 원인: 일반 범위 for의 루프 변수는 상수가 아니라서 스플라이스에 쓸 수 없고, std::vector<std::meta::info>는 컴파일 타임 밖으로 나갈 수 없습니다.

// ✅ template for + define_static_array
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))) {
    use(obj.[:m:]);
}

의존적인 문맥에서 스플라이스가 타입이면 typename [: r :]처럼 typename을 붙여야 한다는 점도 자주 걸리는 부분입니다.

에러 5: std::execution에서 Receiver 연결 누락

증상: Sender만 만들고 connect/start 없이 사용 원인: Sender는 lazy합니다. connect로 Receiver와 연결하고 start를 호출해야 실제로 실행됩니다.

// ❌ 잘못된 코드
auto s = std::execution::schedule(sched) | std::execution::then([] {});
// 아무 일도 안 일어남
// ✅ 올바른 코드: 완료까지 기다리기
auto result = std::this_thread::sync_wait(std::move(s));
// 또는 직접 연결: 연산 상태는 이동할 수 없으므로 그 자리에서 만들고 start
auto op = std::execution::connect(std::move(s), receiver);
std::execution::start(op);   // op는 완료될 때까지 살아 있어야 함

에러 6: 컴파일러/플래그 미지원

증상: ^^, <meta> 등 인식 불가 원인: 2026년 기준 GCC 16이 -std=c++26 -freflection으로 지원하고, Clang은 Bloomberg의 clang-p2996 포크에서만 쓸 수 있으며, MSVC는 아직 지원하지 않습니다.

#if defined(__cpp_impl_reflection)
    // Reflection 사용
#else
    // 폴백: magic_enum, Boost.PFR, 매크로, 코드 생성
#endif

에러 7: identifier_of vs display_string_of 혼동

증상: 반환되는 문자열이 예상과 다름 원인: identifier_of는 선언된 식별자(코드에 쓰인 이름)이고 이름이 없으면 실패합니다. display_string_of는 사람이 읽을 표시용 문자열로, 네임스페이스·템플릿 인자가 포함될 수 있고 형식이 구현마다 다릅니다. JSON 키처럼 안정적인 값이 필요하면 identifier_of, 로그·에러 메시지에는 display_string_of를 씁니다.

에러 8: std::execution에서 예외 처리 누락

증상: Sender 체인에서 예외가 발생해도 처리되지 않음 원인: Receiver의 set_error를 구현해야 예외가 전파됩니다. 기본 Receiver를 그대로 쓰면 예외가 무시되거나 std::terminate가 호출될 수 있습니다.

// ✅ let_error 또는 then으로 에러 처리
auto s = some_sender
    | std::execution::let_error([](std::exception_ptr) {
        // 로깅, 폴백 값 반환 등
        return std::execution::just(default_value);
    });

C++23 코드에 C++26 기능 들이는 순서

점진적 도입

// 1단계: 기능 테스트 매크로로 분기
#if defined(__cpp_impl_reflection)
template <typename T>
std::string to_string(const T& obj) {
    return to_string_reflection(obj);
}
#else
template <typename T>
std::string to_string(const T& obj) {
    return to_string_manual(obj);  // 기존 수동 구현
}
#endif

직렬화 마이그레이션

// Before (C++23): 수동
struct User {
    int id;
    std::string name;
};
std::string to_json(const User& u) {
    return "{\"id\":" + std::to_string(u.id) + ",\"name\":\"" + u.name + "\"}";
}
// After (C++26): Reflection
template <typename T>
std::string to_json(const T& obj) {
    // 멤버 순회로 자동 생성
    return to_json_reflection(obj);
}

코드 생성기와의 공존

// protobuf, Qt MOC 등과 함께 사용
// - Reflection: 새 코드, 작은 구조체
// - 코드 생성: 레거시, 대규모 스키마

의존성 정리

// Boost.Hana, Boost.PFR 등 메타프로그래밍 라이브러리
// → Reflection으로 대체 가능한 부분 식별 후 점진적 제거

단계별 적용 로드맵

1단계: 기능 테스트
  - __cpp_impl_reflection 매크로로 분기
  - 작은 구조체 1~2개에 Reflection 직렬화 적용
2단계: 핵심 레이어 전환
  - 설정, API DTO 등 자주 변경되는 구조체에 적용
  - 기존 수동 코드와 A/B 비교
3단계: 확대
  - 테스트 픽스처, CLI 파싱 등으로 확장
  - 코드 생성기 의존도 감소

CMake/빌드 설정 예시

# CMakeLists.txt
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15)
        add_compile_options(-std=c++26 -freflection)
    endif()
endif()
# 기능 테스트
add_compile_definitions($<$<BOOL:${REFLECTION_AVAILABLE}>:USE_CPP26_REFLECTION=1>)

Reflection 직렬화 레이어, execution 비동기 서버, 조건부 컴파일

패턴 1: Reflection 기반 직렬화 레이어

// config_serializer.hpp
template <typename T>
class ReflectionSerializer {
public:
    static std::string serialize(const T& obj) {
        std::ostringstream oss;
        oss << "{";
        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) oss << ", ";
            oss << std::meta::identifier_of(m) << ": ";
            serialize_value(oss, obj.[:m:]);   // 타입별 오버로드
            first = false;
        }
        oss << "}";
        return oss.str();
    }
};

패턴 2: std::execution 기반 비동기 서버

// 비동기 accept → read → process → write 체인
auto handle_session() {
    return std::execution::schedule(io_scheduler)
        | std::execution::then(accept_connection)
        | std::execution::let_value(read_request)
        | std::execution::let_value(process_request)
        | std::execution::then(send_response);
}

패턴 3: 어노테이션 기반 검증

enum class Validate { required, optional };
struct ApiRequest {
    [[=Validate::required]] std::string token;
    [[=Validate::optional]] std::string filter;
};
// annotations_of_with_type(m, ^^Validate)로 어노테이션을 찾고
// std::meta::extract<Validate>(a)로 값을 꺼내 검증 (예시 5 참고)

패턴 4: 테스트 픽스처 자동 생성

template <typename T>
T create_test_fixture() {
    T obj{};
    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))) {
        set_default(obj.[:m:]);   // 타입별 오버로드 (예시 6 참고)
    }
    return obj;
}

패턴 5: 조건부 컴파일

#ifdef USE_CPP26_REFLECTION
    #define SERIALIZE(...) /* Reflection 기반 */
#else
    #define SERIALIZE(...) /* 매크로/수동 기반 */
#endif

C++26 타임라인과 구현 상태

타임라인

  • 2024년 6월: std::execution(P2300), std::simd 채택
  • 2025년 2월: Contracts(P2900), std::hive 채택
  • 2025년 6월: Reflection(P2996)과 관련 제안 채택, 새 기능 접수 마감
  • 2026년: 국가 기관 의견 처리 후 기술적으로 확정, ISO 발행 절차

구현 상태 (2026년 9월 기준)

컴파일러Reflectionstd::executionContracts
GCCGCC 16, -std=c++26 -freflection진행 중 (참조 구현 stdexec 사용 가능)진행 중
ClangBloomberg clang-p2996 포크 (Compiler Explorer)stdexec로 사용진행 중
MSVC미지원stdexec로 사용미지원

구현 상황은 빠르게 바뀌므로 cppreference의 “C++26 compiler support” 표를 기준으로 확인하세요.

실습 환경 설정

GCC 16:

g++ -std=c++26 -freflection -o demo demo.cpp

Clang (Compiler Explorer): godbolt.org에서 “x86-64 clang (reflection)” 계열 컴파일러를 고르고 -std=c++26 -freflection-latest 같은 안내된 옵션을 사용합니다.

기능 테스트 매크로:

#if defined(__cpp_impl_reflection)
    #define HAS_REFLECTION 1
#else
    #define HAS_REFLECTION 0
#endif

같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. 지금 실무 코드에 Reflection을 넣어도 되나요?

A. 팀의 모든 빌드 대상 컴파일러가 지원할 때까지는 기능 테스트 매크로 뒤에 두는 것이 현실적입니다. GCC 16만 쓰는 리눅스 서버 코드라면 직렬화·로깅 같은 한정된 영역부터 적용해 볼 수 있고, MSVC를 함께 지원해야 한다면 Boost.PFR·magic_enum·매크로 경로를 기본으로 유지하면서 C++26 경로를 추가하는 방식이 안전합니다.

Q. Reflection이 코드 생성기보다 나은가요?

A. 장점: 별도 빌드 단계 없음, 타입 정보와 코드가 한 곳에 있음. 단점: 컴파일러 지원·표준 확정이 필요함. 코드 생성기는 이미 안정적이며, Reflection은 새 프로젝트나 작은 구조체에 먼저 적용하는 것이 현실적입니다.

Q. Reflection이 코드 크기를 늘리나요?

A. 컴파일 타임에 처리되므로 런타임 코드 크기는 수동 직렬화와 유사합니다. 멤버 순회가 템플릿 인스턴스화로 펼쳐지므로, 생성되는 기계어는 수동과 거의 동일합니다.

Q. std::execution은 언제 써야 하나요?

A. 새 비동기 코드를 작성할 때는 std::execution을 우선 고려하며, 기존 std::async 코드는 점진적으로 마이그레이션하는 것이 좋습니다. 조합이 많고 취소·에러 전파가 중요한 경우 std::execution이 유리합니다.


참고 자료


C++26의 Reflection과 std::execution으로 직렬화·비동기 코드를 표준 기능으로 자동화할 수 있습니다. 다음으로 C++·Rust 상호운용(#44-2)를 읽어보면 좋습니다. 다음 글: [C++의 미래 #44-2] C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 이전 글: [실전 도메인 #43-3] Observability: Prometheus와 Grafana로 C++ 서버 모니터링 지표 추출