C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]
들어가며: 직렬화 코드를 매번 손으로 작성해야 한다
C++에서 JSON 직렬화, ORM 바인딩, 디버그 출력, 명령줄 인자 파싱을 구현하면 멤버 변수를 하나씩 나열하는 코드를 반복해서 쓰게 됩니다. 구조체에 필드가 하나 늘면 to_json, from_json, 테스트 기대값을 모두 고쳐야 하고, 하나라도 빠뜨리면 컴파일러는 알려 주지 않습니다. Java나 C#은 실행 시점에 타입 정보를 조회하는 리플렉션이 있지만, C++는 이런 정보를 언어 차원에서 노출하지 않았기 때문에 protobuf나 Qt MOC 같은 코드 생성기, 혹은 매크로에 의존해 왔습니다.
C++26에 채택된 정적 리플렉션(P2996)은 컴파일 타임에 타입의 멤버와 이름을 조회하고, 그 결과로 코드를 만들어 낼 수 있게 합니다. 런타임에 메타데이터를 들고 다니는 Java 방식과 달리 모든 처리가 컴파일 중에 끝나므로 실행 비용이 따로 들지 않습니다. 이 글은 리플렉션의 문법과 예제를 중심으로, 비동기 모델인 std::execution(P2300), Contracts와 그 밖의 C++26 기능, 그리고 C++26에 들어가지 않은 기능을 정리합니다.
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
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) | 특정 타입의 어노테이션 |
열거형 값을 이름으로
지금까지 magic_enum 같은 라이브러리가 컴파일러별 __PRETTY_FUNCTION__ 트릭으로 하던 일을 표준 문법으로 쓸 수 있습니다.
#include <meta>
#include <string_view>
enum class Status { Idle, Running, Stopped };
template <typename 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>";
}
static_assert(enum_name(Status::Running) == "Running");
enumerators_of는 열거자를 선언 순서대로 돌려주고, [:e:]는 해당 열거자 값으로 스플라이스됩니다. magic_enum과 달리 값 범위 제한(기본 -128~127)이 없고, 같은 값을 가진 열거자가 여러 개면 먼저 선언된 이름이 반환됩니다.
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<<가 없는 타입이 있으면 그 자리에서 컴파일 에러가 나므로, 실제로는 타입별 분기(아래 예시 6처럼 if constexpr로 나누는 방식)를 둡니다.
예시 2: 어노테이션으로 CLI 옵션 선언
C++26 어노테이션은 [[=값]] 형태로 상수 값을 선언에 붙입니다. 일반 속성([[nodiscard]])과 달리 값을 리플렉션으로 읽을 수 있어서, Rust의 clap·serde 같은 선언적 설정을 표준 C++로 흉내 낼 수 있습니다.
#include <meta>
#include <iostream>
#include <string>
// 어노테이션 값은 구조적 타입(structural type)이어야 합니다.
// std::string_view는 private 멤버가 있어 구조적 타입이 아니므로
// 문자열은 std::define_static_string으로 만든 정적 문자열 포인터로 담습니다.
struct help { const char* text; };
struct flag { const char* long_name; char short_name = 0; };
struct Args {
[[=help{std::define_static_string("Name to greet")},
=flag{std::define_static_string("name"), 'n'}]] std::string name;
[[=help{std::define_static_string("Number of times to greet")},
=flag{std::define_static_string("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>
#include <string>
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: 실전 — 필수 필드 검증
#include <meta>
#include <string>
#include <vector>
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는 결과를 기다리려면 get()으로 블로킹해야 하고, “이 작업이 끝나면 저 작업” 같은 연결을 표준 방식으로 표현할 수 없으며, 취소 수단도 없습니다. 작업이 어느 스레드에서 실행될지도 구현에 맡겨집니다. P2300 std::execution은 비동기 작업을 sender라는 값으로 표현하고, 이를 파이프(|)로 조합한 뒤 원하는 스케줄러에서 실행하는 모델을 제공합니다.
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_stopped]
N4 --> N5[then, let_value 등으로 조합]
end
핵심 개념
| 개념 | 설명 |
|---|---|
| Sender | 비동기 작업을 나타내는 객체. connect로 Receiver와 연결 |
| Receiver | set_value, set_error, set_stopped 중 하나로 완료 신호를 받음 |
| Operation State | connect 결과. start()로 작업 시작 |
| Scheduler | 작업을 어디서 실행할지 결정 |
기본 예시
// C++26 std::execution (P2300)
#include <execution>
#include <iostream>
#include <utility>
namespace ex = std::execution;
int main() {
auto work = ex::just(21)
| ex::then([](int x) { return x * 2; })
| ex::then([](int x) { std::cout << x << '\n'; });
// sender는 lazy합니다. sync_wait가 receiver를 연결하고 시작한 뒤 완료까지 기다립니다.
std::this_thread::sync_wait(std::move(work)); // 42 출력
}
just는 주어진 값으로 바로 완료되는 sender를, then은 앞 sender의 결과를 받아 함수를 적용하는 sender를 만듭니다. 특정 실행 자원에서 돌리려면 ex::schedule(sched)로 시작하거나 ex::starts_on(sched, work)처럼 스케줄러를 지정합니다. sync_wait는 내부적으로 connect와 start를 호출하고, 오류 채널로 예외가 오면 호출한 쪽에 다시 던집니다.
실전: 비동기 체인
// 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); // let_value의 함수는 sender를 반환해야 함
})
| std::execution::then([](auto result) {
return save_result(result);
});
}
then은 일반 값을 반환하는 함수를, let_value는 또 다른 비동기 작업(sender)을 반환하는 함수를 연결할 때 씁니다. 네트워크 요청, 파싱, DB 저장처럼 중간에 비동기 단계가 끼는 파이프라인이 이런 형태가 됩니다.
여러 작업 합치기: when_all
namespace ex = std::execution;
auto combined = ex::when_all(ex::just(1), ex::just(2), ex::just(3))
| ex::then([](int a, int b, int c) { return a + b + c; });
auto [sum] = std::this_thread::sync_wait(std::move(combined)).value(); // sum == 6
when_all은 모든 자식 sender가 값으로 완료되면, 각 자식의 값을 순서대로 이어 붙여 한 번에 보냅니다. 그래서 다음 then은 튜플이 아니라 (int, int, int) 세 인자를 받습니다. 자식 중 하나가 오류나 취소로 끝나면 나머지에 중지 요청을 보내고 그 결과를 전달합니다. 자식들이 실제로 병렬로 도는지는 각 자식이 어느 스케줄러에서 시작했는지에 달려 있어서, 스레드 풀에서 동시에 돌리려면 자식마다 ex::starts_on(pool_sched, ...)로 실행 자원을 지정해야 합니다. 반대로 작업을 마친 뒤 결과를 UI 스레드 같은 다른 실행 자원으로 넘기려면 ex::continues_on(sched)을 씁니다.
기타 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 체인에서 예외가 발생해도 처리되지 않음
원인: 체인 안의 함수가 던진 예외는 set_error(std::exception_ptr)로 오류 채널을 따라 전달됩니다. sync_wait는 이를 호출한 쪽에 다시 던지지만, 직접 작성한 receiver가 set_error를 제대로 처리하지 않거나 start_detached로 결과를 버리면 오류를 놓치거나 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
기능마다 나중에 옮기는 비용이 다르다는 점도 순서를 정할 때 고려할 만합니다. stdexec는 std::execution 제안을 따라가는 참조 구현이라, 표준 라이브러리가 지원하면 헤더와 네임스페이스를 바꾸는 수준으로 옮기기 쉽습니다. 반면 리플렉션은 매크로·magic_enum과 문법이 완전히 달라 교체 비용이 크므로, 코드베이스 전체에 퍼뜨리기보다 직렬화처럼 한곳에 모인 코드에서 먼저 시험해 보는 편이 좋습니다.
CMake/빌드 설정 예시
# CMakeLists.txt
set(CMAKE_CXX_STANDARD 26)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU"
AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16)
add_compile_options(-freflection)
endif()
# 실제 지원 여부는 코드 안에서 __cpp_impl_reflection으로 다시 확인합니다
C++26 타임라인과 구현 상태
타임라인
- 2024년 6월: std::execution(P2300) 채택
- 2025년 2월: Contracts(P2900), std::hive 채택
- 2025년 6월: Reflection(P2996)과 관련 제안 채택, 새 기능 접수 마감
- 2026년: 국가 기관 의견 처리 후 기술적으로 확정, ISO 발행 절차
구현 상태 (2026년 9월 기준)
| 컴파일러 | Reflection | std::execution | Contracts |
|---|---|---|---|
| GCC | GCC 16, -std=c++26 -freflection | 진행 중 (참조 구현 stdexec 사용 가능) | 진행 중 |
| Clang | Bloomberg 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
같이 보면 좋은 글
- C++ 컴파일 타임 리플렉션: C++26 Reflection·magic_enum·매크로
- C++26 리플렉션 기초
- SFINAE 패턴 정리
- C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]
- C++ 가변 인자 템플릿: 파라미터 팩 재귀 전개, Fold Expression, 완벽한 전달
- constexpr 기초부터 C++26까지
Reflection과 코드 생성기, std::async 비교
리플렉션은 별도 빌드 단계 없이 타입 정의와 직렬화 로직이 같은 코드 안에 있다는 점이 코드 생성기보다 낫습니다. 반면 protobuf 같은 코드 생성기는 이미 안정적이고, 언어 간 스키마 공유와 버전 호환 규칙까지 제공하므로 대체 대상이 아닙니다. 새로 만드는 내부 구조체의 직렬화·로깅부터 리플렉션을 적용하는 편이 현실적입니다.
리플렉션은 컴파일 중에 멤버 순회를 펼쳐서 일반 코드로 만들기 때문에, 생성되는 기계어는 같은 일을 손으로 쓴 코드와 비슷한 수준입니다. 대신 컴파일 시간이 늘어날 수 있으므로 헤더에 넣은 범용 템플릿이 많은 번역 단위에서 인스턴스화되는지 살펴봐야 합니다.
std::execution은 조합이 많고 취소와 오류 전파가 중요한 새 비동기 코드에 먼저 고려할 만합니다. 표준 라이브러리 구현이 갖춰지기 전까지는 참조 구현인 NVIDIA stdexec로 같은 API를 써 볼 수 있습니다.
참고 자료
- P2996r12 — Reflection for C++26
- P2300 — std::execution (Sender/Receiver)
- P3687 — Final Adjustments to C++26 Reflection
- Learn Modern C++ — Reflection in C++26
- cppreference — Execution control library
다음 글: [C++의 미래 #44-2] C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체
이전 글: [실전 도메인 #43-3] Observability: Prometheus와 Grafana로 C++ 서버 모니터링 지표 추출