C++26에서 달라지는 것: 리플렉션(^^), std::execution Sender/Receiver, std::simd, Contracts
들어가며: C++26이 왜 필요한가?
C++26은 2026년 표준 예정으로, 리플렉션, std::execution, Sender/Receiver, std::simd 등 여러 기능이 추가됩니다. 이 기능들은 반복 코드 제거, 비동기 처리 통합, SIMD 병렬화의 실무 문제를 해결합니다. 비유하면: “도서관의 모든 책을 자동으로 카탈로그화하는 시스템”이 있는데, C++에는 그런 카탈로그가 없어서 책마다 직접 목록을 손으로 적어 두는 상황입니다. 컴파일 타임 리플렉션이 있으면, 타입 선언만으로 멤버 정보를 자동으로 추출할 수 있습니다.
flowchart LR
subgraph problem[문제 상황]
P1[User 구조체] --> P2[to_json 수동 작성]
P3[비동기 API] --> P4[콜백 지옥]
P5[배열 연산] --> P6[루프 병목]
end
subgraph solution[C++26 해결]
S1[^^ 리플렉션] --> S2[자동 직렬화]
S3["std execution"] --> S4[Sender/Receiver]
S5["std simd"] --> S6[벡터화]
end
이 글에서 다루는 것:
-
문제 시나리오: 직렬화, 비동기, SIMD에서 겪는 문제
-
리플렉션 (^^):
std::meta::info, 스플라이싱[: :] -
std::execution: Scheduler, Sender, Receiver,
just,then,schedule -
std::simd:
std::datapar::simd,unchecked_load,reduce -
std::hive: 이터레이터 안정 컨테이너
-
Contracts:
pre,post,contract_assert -
일반적인 에러와 베스트 프랙티스
-
프로덕션 패턴: 폴백 전략, CPU 디스패치, 성능 비교 이 글을 읽으면:
-
C++26 핵심 기능의 개념과 사용법을 이해할 수 있습니다.
-
실험적 브랜치나 stdexec로 미리 경험할 수 있습니다.
-
프로덕션에서 폴백 전략을 적용할 수 있습니다.
직렬화 보일러플레이트, 콜백 지옥, 느린 루프: C++26이 푸는 문제
실제 겪는 상황
"구조체마다 to_json·from_json을 수동 작성하면 멤버 추가 시 누락해요."
"비동기 API가 콜백 지옥입니다. then 체이닝이 되면 좋겠습니다."
"100만 개 float 배열 덧셈이 프로파일에서 30%를 차지해요."
"이미지 픽셀 처리 루프가 너무 느려요."
"컴파일러가 벡터화했다고 하는데 실제로는 스칼라 코드가 나와요."
시나리오별 상세 문제와 해결
API 응답 구조체 직렬화
수십 개 DTO마다 to_json·from_json 수동 작성 시 멤버 추가 누락이 발생합니다. 리플렉션으로 멤버를 자동 순회하면 선언만으로 직렬화가 완성됩니다.
비동기 DB·API 콜백 지옥
“쿼리 실행 → 파싱 → 다음 쿼리 → 병합”이 중첩되면 복잡해집니다. std::execution의 Sender/Receiver로 then·let_value 체이닝하면 선형적으로 표현할 수 있습니다.
오디오/이미지 샘플 처리
스칼라 루프가 프로파일 30% 이상을 차지합니다. std::simd로 이식 가능한 벡터화를 표현하며, par_unseq와 함께 멀티코어·SIMD를 활용할 수 있습니다.
게임 엔티티 컨테이너
std::vector erase 시 이터레이터 무효화, std::list는 캐시 효율이 나쁩니다. std::hive는 O(1) 삭제와 이터레이터 안정성을 제공합니다.
계약 검증 누락
입력 검증을 함수마다 수동 작성하면 누락되기 쉽습니다. Contracts로 pre·post를 선언하면 자동 검증할 수 있습니다.
시나리오별 해결 방향
| 시나리오 | 특징 | C++26 해결 |
|---|---|---|
| 직렬화·검증 | 구조체 멤버 자동 순회 | 리플렉션 ^^, std::meta |
| 비동기 처리 | 콜백 지옥, 에러 전파 | std::execution, Sender/Receiver |
| 배열 연산 | SIMD 병목, 멀티코어 미활용 | std::simd, par_unseq |
| 엔티티 컨테이너 | erase 시 이터레이터 무효화 | std::hive |
| 계약 검증 | 전제조건·사후조건 누락 | Contracts (pre, post) |
Before/After: 직렬화 예시
Before (수동 직렬화): 멤버 추가 시마다 to_json을 수정해야 합니다.
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();
}
After (C++26 리플렉션): 멤버를 자동으로 순회합니다.
#include <meta>
#include <sstream>
// ✅ C++26: 멤버 자동 순회 (채택된 P2996 문법)
template <typename T>
std::string to_json(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) << "\":";
oss << obj.[:m:]; // 스플라이스: obj.id, obj.name ... 로 컴파일됨 (문자열 따옴표 처리는 아래 완전한 예제 참고)
first = false;
}
oss << "}";
return oss.str();
}
C++26 리플렉션 (^^)
C++26에 채택된 리플렉션
P2996 “Reflection for C++26”이 2025년 6월 C++26에 채택되었습니다. 함께 쓰는 확장 문 template for(P1306), std::define_static_array(P3491), 어노테이션 [[=...]](P3394), 접근 문맥 std::meta::access_context(P3547)도 같이 들어갔습니다. 2026년 기준 GCC 16이 -std=c++26 -freflection으로, Clang은 Bloomberg의 clang-p2996 포크가 구현을 제공하며(Compiler Explorer에서 실험 가능), MSVC는 아직 지원하지 않습니다.
주의할 점은 표준화 과정에서 API가 여러 번 바뀌었다는 것입니다. std::meta::name_v<^^T>, nonstatic_data_members_of<^^T> 같은 템플릿 변수 형태, 일반 for (constexpr ...) 루프, consteval 변수 선언은 초기 제안이나 추측으로 쓰인 문법이라 채택된 C++26에서는 컴파일되지 않습니다. 아래 예제는 채택된 형태입니다.
핵심 개념
1. 반사 연산자 ^^
^^를 적용하면 std::meta::info 타입의 컴파일 타임 값이 생깁니다. 이 값은 해당 엔티티를 고유하게 식별합니다. consteval은 함수 지정자라 변수에는 붙일 수 없고, info를 담는 변수는 constexpr로 선언합니다.
#include <meta>
struct Point { int x; int y; };
constexpr std::meta::info point_info = ^^Point;
constexpr std::meta::info x_info = ^^Point::x;
2. std::meta::info
<meta> 헤더에 정의된 불투명 스칼라 타입입니다. 컴파일 타임에만 존재하며, 같은 엔티티를 반사한 값끼리는 ==로 같습니다.
3. 메타함수
consteval 함수로 이름·타입·멤버 목록을 조회합니다.
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);
constexpr auto ctx = std::meta::access_context::current(); // 이 위치에서 접근 가능한 멤버만
static_assert(std::meta::nonstatic_data_members_of(^^Point, ctx).size() == 2);
4. 스플라이스 [: ... :]
info를 다시 코드로 바꿉니다.
typename [: std::meta::type_of(^^Point::x) :] new_variable = 0; // int new_variable = 0;
Point p{24, 42};
std::cout << p.[: ^^Point::y :] << '\n'; // 42 (p.y와 동일)
5. template for
멤버 목록처럼 컴파일 타임에 정해지는 범위를 원소마다 펼칩니다. nonstatic_data_members_of가 돌려주는 std::vector는 컴파일 타임에만 존재할 수 있으므로 std::define_static_array로 감싸서 넘깁니다.
C++26 리플렉션 흐름
flowchart TD A[타입/멤버 선언] --> B["^^ 연산자"] B --> C["std::meta::info"] C --> D["identifier_of, nonstatic_data_members_of 등"] D --> E["template for + 스플라이스 [: :]"] E --> F[직렬화/검증/코드 생성]
완전한 예제: 자동 JSON 직렬화
#include <meta>
#include <iostream>
#include <string>
#include <type_traits>
struct User {
int id;
std::string name;
std::string email;
};
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 += "\"" + std::string(std::meta::identifier_of(m)) + "\":";
using M = std::remove_cvref_t<decltype(obj.[:m:])>;
if constexpr (std::is_same_v<M, std::string>) out += "\"" + obj.[:m:] + "\"";
else if constexpr (std::is_arithmetic_v<M>) out += std::to_string(obj.[:m:]);
else out += to_json(obj.[:m:]);
}
return out + "}";
}
int main() {
User u{1, "Alice", "[email protected]"};
std::cout << to_json(u) << "\n";
// {"id":1,"name":"Alice","email":"[email protected]"}
}
멤버 타입 분기는 std::meta::type_of(m) == ^^std::string처럼 info끼리 비교할 수도 있지만, std::string이 별칭이라 비교가 기대와 다를 수 있습니다. 위처럼 decltype으로 실제 타입을 얻어 std::is_same_v로 비교하는 편이 실수가 적습니다.
enum 반사 (C++26)
#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");
기능 테스트 매크로
#if defined(__cpp_impl_reflection)
// C++26 리플렉션 언어 지원
#endif
#if defined(__cpp_lib_reflection)
// <meta> 라이브러리 지원
#endif
초기 구현의 매크로 값은 버전에 따라 다를 수 있으므로, 특정 숫자와 비교하기보다 정의 여부로 분기하는 편이 안전합니다. 문법 전반은 C++26 리플렉션 기초(#26-1)에 더 자세히 정리했습니다.
std::execution과 Sender/Receiver
std::execution 개요
P2300 “std::execution”이 C++26에 채택되었습니다. Sender, Receiver, Scheduler를 기반으로 비동기 작업을 조합 가능하게 만듭니다. CPU, GPU, I/O 등 다양한 실행 리소스에 통합 적용할 수 있습니다.
핵심 개념
| 개념 | 설명 |
|---|---|
| Sender | 비동기 작업을 설명하는 객체. 연결 후 시작 시 완료 시그널을 보냄 |
| Receiver | Sender의 완료(값·에러·취소)를 수신하는 콜백 |
| Scheduler | 실행 컨텍스트(스레드 풀 등)의 핸들. schedule()로 Sender 생성 |
| Operation State | Sender와 Receiver를 connect한 결과. start()로 실행 시작 |
Sender/Receiver 흐름
flowchart LR
subgraph sender[Sender]
S1[just] --> S2[then]
S2 --> S3[schedule]
end
subgraph receiver[Receiver]
R1[set_value] --> R2[set_error]
R2 --> R3[set_stopped]
end
S3 -->|connect| O[Operation State]
O -->|start| R1
완전한 예제: just, then, schedule
#include <execution>
#include <thread>
#include <iostream>
int main() {
// 1. just: 즉시 값을 전달하는 Sender
auto s1 = std::execution::just(42);
// 2. then: 이전 결과를 변환
auto s2 = std::execution::just(10)
| std::execution::then([](int x) { return x * 2; });
// 3. sync_wait: 동기적으로 실행 완료 대기
auto result = std::this_thread::sync_wait(s2);
std::cout << *result << '\n'; // 20
// 4. schedule: 스케줄러에서 실행
auto sched = std::execution::run_loop{};
auto s3 = std::execution::schedule(sched.get_scheduler())
| std::execution::then([] { std::cout << "Scheduled!\n"; });
sched.run_one(); // "Scheduled!" 출력
return 0;
}
Sender 체이닝: when_all, let_value
#include <execution>
#include <thread>
#include <iostream>
int main() {
// 5. when_all: 여러 Sender를 병렬로 실행
auto s1 = std::execution::just(1);
auto s2 = std::execution::just(2);
auto s3 = std::execution::just(3);
auto combined = std::execution::when_all(s1, s2, s3)
| std::execution::then([](auto a, auto b, auto c) {
return std::get<0>(a) + std::get<0>(b) + std::get<0>(c);
});
auto result = std::this_thread::sync_wait(combined);
std::cout << *result << '\n'; // 6
// 6. let_value: 이전 결과를 받아 새 Sender 생성
auto s4 = std::execution::just(42)
| std::execution::let_value([](int x) {
return std::execution::just(x + 1);
});
auto r = std::this_thread::sync_wait(s4);
std::cout << *r << '\n'; // 43
return 0;
}
에러 처리: upon_error, let_error
#include <execution>
#include <thread>
#include <iostream>
int main() {
// 7. just_error: 에러를 전달하는 Sender
auto err_sender = std::execution::just_error(std::runtime_error("fail"));
// 8. upon_error: 에러 처리
auto handled = err_sender
| std::execution::upon_error([](std::exception_ptr e) {
try { std::rethrow_exception(e); }
catch (const std::exception& ex) {
std::cerr << "Caught: " << ex.what() << '\n';
}
});
// 9. let_error: 에러를 받아 새 Sender로 반환
auto recovered = err_sender
| std::execution::let_error([](std::exception_ptr) {
return std::execution::just(0); // 폴백 값
});
auto r = std::this_thread::sync_wait(recovered);
std::cout << *r << '\n'; // 0
return 0;
}
Sender 알고리즘 요약
| 함수 | 설명 |
|---|---|
just(v...) | 즉시 값을 전달하는 Sender |
just_error(e) | 에러를 전달 |
just_stopped() | 취소 완료 |
schedule(sched) | 스케줄러에서 실행 |
then(sender, fn) | 완료 후 fn 적용 |
let_value(sender, fn) | 완료 값을 받아 새 Sender 반환 |
when_all(senders...) | 여러 Sender 병렬 실행 |
starts_on(sched, sender) | 특정 스케줄러에서 시작 |
continues_on(sched, sender) | 완료 후 다른 스케줄러로 |
std::simd
std::simd 개요
C++26에서 std::datapar::simd 등 데이터 병렬 타입이 표준에 추가됩니다. 한 번에 여러 원소를 처리하는 SIMD 연산을 이식 가능하게 표현합니다. Parallelism TS 2 기반입니다.
핵심 개념
| 타입 | 설명 |
|---|---|
std::datapar::simd<T> | T 타입의 여러 원소를 담는 벡터 |
std::datapar::simd_mask<T> | simd에 대응하는 bool 마스크 |
unchecked_load | 정렬된 메모리에서 로드 |
partial_load | 부분 로드 (마지막 청크 등) |
reduce | 벡터를 스칼라로 축소 |
std::simd 흐름
flowchart LR A[float 배열] --> B[unchecked_load] B --> C[simd + simd] C --> D[reduce] D --> E[스칼라 결과]
완전한 예제: 배열 덧셈
#include <experimental/simd>
#include <vector>
#include <numeric>
#include <iostream>
namespace simd = std::experimental::parallelism_v2;
void add_arrays_simd(const float* a, const float* b, float* out, size_t n) {
using V = simd::native_simd<float>;
auto width = V::size();
size_t i = 0;
for (; i + width <= n; i += width) {
V va = simd::simd_load(a + i);
V vb = simd::simd_load(b + i);
V vc = va + vb;
simd::simd_store(vc, out + i);
}
// 나머지 스칼라 처리
for (; i < n; ++i) {
out[i] = a[i] + b[i];
}
}
int main() {
std::vector<float> a(1000, 1.0f), b(1000, 2.0f), c(1000);
add_arrays_simd(a.data(), b.data(), c.data(), a.size());
std::cout << c[0] << '\n'; // 3
return 0;
}
참고: C++26 std::datapar는 std::experimental::parallelism_v2와 유사합니다. 실제 C++26에서는 <simd> 헤더와 std::datapar::simd를 사용합니다.
C++26 std::datapar::simd (표준 문법)
#include <simd>
#include <vector>
#include <iostream>
int main() {
using V = std::datapar::simd<float>;
std::vector<float> a(1000, 1.0f), b(1000, 2.0f), c(1000);
V va = std::datapar::unchecked_load(a.data());
V vb = std::datapar::unchecked_load(b.data());
V vc = va + vb;
std::datapar::unchecked_store(vc, c.data());
// reduce: 벡터 합
float sum = std::datapar::reduce(va, std::plus{});
// select: 마스크 기반 선택
auto mask = va > 0.5f;
V selected = std::datapar::select(mask, va, vb);
return 0;
}
std::simd와 std::execution::par_unseq 조합
#include <algorithm>
#include <execution>
#include <vector>
#include <numeric>
int main() {
std::vector<float> a(1000000, 1.0f), b(1000000, 2.0f), c(a.size());
// par_unseq: 병렬 + SIMD 벡터화 허용
std::transform(std::execution::par_unseq,
a.begin(), a.end(), b.begin(), c.begin(),
[](float x, float y) { return x + y; });
float sum = std::reduce(std::execution::par_unseq, c.begin(), c.end());
return 0;
}
std::simd 알고리즘 요약
| 함수 | 설명 |
|---|---|
unchecked_load(ptr) | 정렬된 메모리에서 로드 |
partial_load(ptr, n) | n개 원소만 로드 |
unchecked_store(ptr) | 정렬된 메모리에 저장 |
reduce(simd, op) | 스칼라로 축소 |
simd::min, simd::max | 원소별 최소·최대 |
simd::select(mask, a, b) | 마스크 조건 선택 |
simd::all_of, any_of | 마스크 축소 |
C++26 기타 기능 (std::hive, Contracts)
std::hive: 이터레이터 안정 컨테이너
C++26에서 std::hive가 추가됩니다. P0447 기반으로, O(1) 삽입·삭제와 삭제 시에도 다른 원소의 이터레이터·포인터·참조가 유효한 컨테이너입니다. 게임 엔티티, 이벤트 핸들러, 노드 기반 자료구조에 적합합니다.
#include <hive>
#include <iostream>
// 변수 선언 및 초기화
int main() {
std::hive<int> h;
h.insert(1);
h.insert(2);
auto it = h.insert(3);
// it이 가리키는 3을 삭제해도, 1과 2의 이터레이터는 유효
h.erase(it);
for (int x : h) {
std::cout << x << ' '; // 1 2 (순서는 구현 정의)
}
return 0;
}
std::hive vs std::vector vs std::list:
| 특성 | std::vector | std::list | std::hive |
|---|---|---|---|
| 삽입/삭제 | O(n) (이동) | O(1) | O(1) |
| 이터레이터 안정성 | erase 시 무효화 | 안정 | 안정 |
| 캐시 효율 | 우수 | 나쁨 | 블록 기반으로 양호 |
| 랜덤 액세스 | O(1) | 없음 | 없음 |
Contracts: 전제조건·사후조건
C++26 Contracts는 pre(전제조건), post(사후조건), contract_assert를 지원합니다. 설계-by-계약으로 버그를 조기에 발견할 수 있습니다.
#include <contracts>
// 전제조건: x >= 0
// 사후조건: 반환값 >= 0
int sqrt_approx(int x)
pre(x >= 0)
post(result: result >= 0)
{
contract_assert(x < 10000); // 추가 검증
// ... 구현
return result;
}
주의: Contracts는 실험적이며, 컴파일러·빌드 모드에 따라 검사 여부가 다릅니다. assert 또는 수동 검증으로 폴백하세요.
리플렉션 문법, std::execution 미지원, simd 정렬 위반 같은 에러
에러 1: C++26 리플렉션 문법 오류
원인: 리플렉션을 구현하지 않은 컴파일러(GCC 15 이하, 본가 Clang, MSVC)에서 쓰거나, 채택 이전의 옛 문법 예제를 복사한 경우입니다.
// ❌ 옛 문법: 채택된 C++26에는 없음
consteval auto info = ^^MyStruct; // 변수에 consteval 불가
auto n = std::meta::name_v<^^MyStruct>; // name_v 없음
// ✅ 채택된 문법 + 기능 테스트 매크로로 폴백
#if defined(__cpp_impl_reflection)
constexpr auto info = ^^MyStruct;
constexpr auto n = std::meta::identifier_of(info);
#else
// magic_enum 또는 매크로 사용
#endif
에러 2: std::execution 미지원
원인: C++26 std::execution이 아직 구현되지 않음. GCC, Clang, MSVC 모두 2025년 기준 미지원.
// ❌ 표준 라이브러리에 없음
#include <execution> // C++17 execution은 std::execution::par 등
// C++26 execution은 Sender/Receiver
// ✅ 해결: stdexec 사용
// vcpkg install stdexec
#include <stdexec/execution.hpp>
namespace exec = stdexec;
에러 3: std::simd unchecked_load 정렬 위반
원인: unchecked_load는 정렬된 메모리 필요. 비정렬 주소 사용 시 크래시.
// ❌ 잘못된 예
float* ptr = new float[100]; // 정렬 보장 안 됨
auto v = std::datapar::unchecked_load(ptr); // UB!
// ✅ 해결: aligned_alloc 또는 std::vector
alignas(64) std::array<float, 100> arr;
auto v = std::datapar::unchecked_load(arr.data());
// 또는 partial_load로 마지막 청크 처리
에러 4: par_unseq에서 동기화 사용
원인: par_unseq는 람다 내부에서 std::mutex, std::atomic 등 동기화 금지.
// ❌ UB
std::mutex mtx;
std::transform(std::execution::par_unseq, a.begin(), a.end(), c.begin(),
[&mtx](float x) {
std::lock_guard<std::mutex> lock(mtx); // UB!
return x * 2;
});
// ✅ 해결: par만 사용하거나, 람다에서 동기화 제거
std::transform(std::execution::par, a.begin(), a.end(), c.begin(),
[](float x) { return x * 2; });
에러 5: Sender를 start하지 않음
원인: connect만 하고 start를 호출하지 않으면 작업이 실행되지 않음.
// ❌ 잘못된 예: connect만 하고 start() 호출 안 함
auto s = std::execution::just(42);
// auto state = std::execution::connect(s, some_receiver);
// std::execution::start(state); // 이 호출이 없으면 작업 실행 안 됨
// ✅ 해결: sync_wait 사용 (내부에서 connect+start)
auto result = std::this_thread::sync_wait(std::execution::just(42));
에러 6: std::simd CPU 기능 미검사
원인: AVX 코드를 AVX 미지원 CPU에서 실행 → SIGILL.
// ❌ AVX 미지원 CPU에서 크래시
__m256 va = _mm256_load_ps(ptr);
// ✅ 해결: CPU 디스패치
#if __AVX__
// AVX 경로
#else
// SSE 또는 스칼라 폴백
#endif
에러 7: 리플렉션으로 private 멤버 접근
원인: 멤버를 나열할 때 넘긴 access_context가 의도와 다른 경우입니다. access_context::current()는 호출 위치에서 접근 가능한 멤버만 돌려주므로 클래스 밖에서는 private 멤버가 목록에 나오지 않고, access_context::unchecked()는 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:]);
}
// 직렬화 라이브러리처럼 모든 필드가 필요한 곳에서만 unchecked()를 명시적으로 사용
에러 8: Sender에서 예외 전파
원인: Sender의 then 람다에서 예외 throw 시 set_error로 전파됩니다. sync_wait에서 예외 재throw.
// ✅ 예외 처리
auto s = std::execution::just(42)
| std::execution::then([](int x) {
if (x < 0) throw std::runtime_error("invalid");
return x;
});
try {
auto r = std::this_thread::sync_wait(s);
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
에러 9: std::hive erase 후 무효화된 이터레이터 사용
원인: erase(it) 호출 시 it은 무효화됩니다. 해결: it = h.erase(it)로 erase가 반환하는 다음 유효 이터레이터를 사용하세요.
에러 10: partial_load 경계 오버런
원인: 마지막 청크가 simd 너비보다 작을 때 unchecked_load로 전체 로드하면 버퍼 오버런. 해결: partial_load(a + i, n - i) 또는 스칼라 루프로 나머지 처리.
에러 11: when_all에서 tuple 인자 해체 실수
원인: when_all 결과는 std::tuple<std::tuple<T1>, std::tuple<T2>, ...> 형태입니다. std::get<0>(a) 등으로 내부 값을 꺼내야 합니다.
환경별 폴백: magic_enum, stdexec, xsimd
환경별 폴백 전략
#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이 가장 실용적입니다.
// vcpkg: vcpkg install magic-enum
#include <magic_enum.hpp>
enum class Status { Idle, Running, Stopped };
auto name = magic_enum::enum_name(Status::Running); // "Running"
std::execution은 stdexec로 미리 경험
// stdexec는 P2300 참조 구현
// CMake: add_subdirectory(stdexec) 또는 vcpkg
#include <stdexec/execution.hpp>
#include <exec/static_thread_pool.hpp>
exec::static_thread_pool pool{4};
auto sched = pool.get_scheduler();
std::simd는 xsimd로 폴백
#if __cpp_lib_experimental_simd >= 201803L
#include <experimental/simd>
namespace simd_ns = std::experimental::parallelism_v2;
#else
#include <xsimd/xsimd.hpp>
namespace simd_ns = xsimd;
#endif
CPU 디스패치로 SIMD 경로 선택
void process(float* data, size_t n) {
#if defined(__AVX__)
process_avx(data, n);
#elif defined(__SSE__)
process_sse(data, n);
#else
process_scalar(data, n);
#endif
}
Sender 체이닝 시 에러 처리 명시
auto pipeline = std::execution::just(input)
| std::execution::then(parse)
| std::execution::upon_error([](std::exception_ptr) {
std::cerr << "Parse error\n";
})
| std::execution::then(validate)
| std::execution::let_error([](std::exception_ptr) {
return std::execution::just(default_value);
});
static_assert로 멤버 개수 검증
struct User {
int id;
std::string name;
std::string email;
};
// 매크로/수동 등록 시 누락 방지
static_assert(std::tuple_size_v<decltype(as_tuple(std::declval<User>()))> == 3,
"User must have exactly 3 members");
리플렉션 타입별 분기
멤버 타입에 따라 직렬화 방식을 다르게 할 때 if constexpr로 분기합니다. using M = std::remove_cvref_t<decltype(obj.[:m:])>;로 실제 타입을 얻어 std::is_same_v<M, std::string>처럼 비교하는 방식이 별칭 문제 없이 가장 확실합니다.
설정 로드, stdexec 파이프라인, SIMD 픽셀 처리, std::hive
패턴 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: 비동기 파이프라인 (stdexec)
#include <stdexec/execution.hpp>
#include <exec/static_thread_pool.hpp>
exec::static_thread_pool pool{4};
auto async_process(std::string input) {
return stdexec::just(std::move(input))
| stdexec::then([](std::string s) { return parse(s); })
| stdexec::then([](auto p) { return validate(p); })
| stdexec::let_value([](auto v) {
return stdexec::schedule(pool.get_scheduler())
| stdexec::then([v] { return process(v); });
});
}
패턴 3: 이미지 픽셀 처리 (SIMD + par_unseq)
#include <algorithm>
#include <execution>
#include <vector>
void normalize_pixels(std::vector<float>& pixels, float max_val) {
std::transform(std::execution::par_unseq,
pixels.begin(), pixels.end(), pixels.begin(),
[max_val](float p) { return p / max_val; });
}
float sum_pixels(const std::vector<float>& pixels) {
return std::reduce(std::execution::par_unseq,
pixels.begin(), pixels.end(), 0.0f);
}
패턴 4: 프로토콜 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));
}
패턴 5: CPU 디스패치 + SIMD 폴백
#if __AVX2__
void process_block_avx2(const float* in, float* out, size_t n) {
// AVX2 인트린직
}
#endif
#if __SSE4_1__
void process_block_sse(const float* in, float* out, size_t n) {
// SSE 인트린직
}
#endif
void process_block_scalar(const float* in, float* out, size_t n) {
for (size_t i = 0; i < n; ++i) out[i] = in[i] * 2;
}
void process(const float* in, float* out, size_t n) {
#if __AVX2__
process_block_avx2(in, out, n);
#elif __SSE4_1__
process_block_sse(in, out, n);
#else
process_block_scalar(in, out, n);
#endif
}
패턴 6: Sender when_all로 병렬 I/O
// 개념적 예: 여러 HTTP 요청 병렬 실행
auto fetch_all = std::execution::when_all(
fetch_url("https://api.a.com"),
fetch_url("https://api.b.com"),
fetch_url("https://api.c.com")
) | std::execution::then([](auto a, auto b, auto c) {
return combine(std::get<0>(a), std::get<0>(b), std::get<0>(c));
});
auto results = std::this_thread::sync_wait(fetch_all);
패턴 7: std::hive로 엔티티 관리
#include <hive>
struct Entity { int id; float x, y; };
std::hive<Entity> g_entities;
Entity* spawn(int id, float x, float y) {
return &*g_entities.insert({id, x, y});
}
void destroy(Entity* e) {
for (auto it = g_entities.begin(); it != g_entities.end(); ++it)
if (&*it == e) { g_entities.erase(it); return; }
}
도입 전 점검 항목
- C++26 기능 테스트 매크로로 폴백 분기
- 리플렉션: magic_enum 또는 매크로 대체
- std::execution: stdexec 또는 std::async 대체
- std::simd: xsimd 또는 인트린직 대체
- CPU 디스패치 (AVX/SSE/스칼라)
- par_unseq 시 동기화 제거
- Sender 에러 처리 (upon_error, let_error)
SIMD, std::hive, Sender/Receiver 성능 비교
SIMD: 스칼라 vs par_unseq vs 수동 SIMD
100만 개 float 배열 덧셈 기준 (Apple M1, GCC 14 -O3):
| 방식 | 상대 시간 | 비고 |
|---|---|---|
| 스칼라 for 루프 | 1.0x | 기준 |
std::transform seq | ~0.95x | 컴파일러 자동 벡터화 |
std::transform par_unseq | ~0.15x | 멀티코어 + SIMD |
| 수동 xsimd/native_simd | ~0.12x | 최대 제어 |
std::hive vs std::vector (빈번한 erase)
엔티티 10만 개에서 5만 개를 랜덤 삭제할 때:
| 컨테이너 | 삭제 시간 | 순회 시간 |
|---|---|---|
| std::vector (erase + 이동) | O(n²) 수준 | 빠름 |
| std::list | O(n) | 느림 (캐시 미스) |
| std::hive | O(n) | vector에 근접 |
Sender/Receiver vs std::async
std::async는 future 중첩 시 복잡해지고 에러 전파가 수동입니다. Sender/Receiver는 upon_error·let_error로 조합이 선형적입니다.
지금 C++26 기능을 도입할지 정하는 기준
표준에 채택됐다는 것과 여러분의 툴체인에서 쓸 수 있다는 것은 별개입니다. 배포 대상 컴파일러가 해당 기능을 정식으로 지원하기 전까지는 앞의 폴백 절처럼 magic_enum·stdexec·xsimd 같은 라이브러리로 같은 프로그래밍 모델을 먼저 쓰고, 호출부는 얇은 래퍼 뒤에 숨겨 두는 편이 안전합니다.
기능마다 나중에 옮기는 비용이 다르다는 점도 고려해야 합니다. stdexec는 std::execution 제안의 참조 구현에 가까워서 표준 라이브러리가 따라오면 네임스페이스와 헤더를 바꾸는 수준으로 옮기기 쉽습니다. 반면 리플렉션은 매크로·magic_enum과 문법이 완전히 달라 교체 비용이 크므로, 코드베이스 전체에 퍼뜨리기보다 직렬화처럼 한곳에 모인 코드에서 먼저 시험해 보는 것이 좋습니다.
어떤 기능이 어느 컴파일러 버전에서 켜지는지는 빠르게 바뀝니다. 도입 직전에 cppreference의 컴파일러 지원 표와 __cpp_*·__cpp_lib_* 기능 테스트 매크로로 직접 확인하고, 매크로로 분기해 폴백 경로를 남겨 두세요.
자주 묻는 질문 (FAQ)
Q. C++26 기능을 지금 쓸 수 있나요?
A. 기능은 모두 확정되었지만 컴파일러·표준 라이브러리 구현은 기능마다 속도가 다릅니다. 리플렉션은 GCC 16과 clang-p2996 포크에서 쓸 수 있고 MSVC는 아직입니다. std::execution은 NVIDIA의 stdexec(참조 구현)으로, std::simd는 libstdc++의 <experimental/simd>나 xsimd·Highway로 미리 써 볼 수 있습니다. 각 컴파일러의 C++26 지원 현황은 cppreference의 “C++26 compiler support” 표가 가장 빨리 갱신됩니다.
Q. 프로덕션에서 바로 써도 되나요?
A. std::execution·std::simd는 stdexec·xsimd 등으로 폴백하며, 리플렉션은 magic_enum·매크로로 대체할 수 있습니다. 기능 테스트 매크로로 분기하면 됩니다.
Q. std::hive와 std::list 차이는?
A. std::hive는 블록 기반으로 캐시 효율이 좋으며, std::list는 노드마다 할당해 캐시 미스가 많습니다. hive가 순회·삽입·삭제 모두에서 일반적으로 더 빠릅니다.
참고 자료
- P2996 Reflection for C++26
- P2300 std::execution
- cppreference: Execution control library
- cppreference: Data-parallel types (SIMD)
- P0447 std::hive
- stdexec (GitHub)
- xsimd (GitHub)
같이 보면 좋은 글
- C++ 컴파일 타임 리플렉션 | C++26 Reflection·magic_enum·매크로 직렬화·검증
- C++ SIMD와 병렬화: std::execution과 인트린직 가이드
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++23에서 실제로 쓸 기능
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++ 컨테이너 기반 개발: Docker로 빌드 환경 표준화 및 배포 이미지 최적화 [#40-3]