C++26 표준 확정: 리플렉션·Contracts·std::execution 등 주요 변경점
이 글의 핵심
C++26은 리플렉션과 Contracts처럼 코드 작성 방식을 바꾸는 기능이 한꺼번에 들어온 표준입니다. 이 글은 주요 기능이 각각 어떤 문제를 풀려고 도입됐는지 설명하고, 재컴파일이나 빌드 설정만으로 얻을 수 있는 안전성 개선과 코드를 고쳐야 얻는 이점을 구분하며, 2026년 9월 기준 컴파일러 지원 상황을 정리합니다.
들어가며
C++26의 기능 목록이 확정되었습니다. WG21(ISO C++ 위원회)은 2025년 6월 불가리아 소피아 회의에서 C++26의 기능 추가를 마감(feature complete)했고, 그 뒤로는 국가 표준 기관들의 검토 의견을 처리하며 문구를 다듬는 단계를 거쳤습니다. 기술적인 작업이 끝나면 최종 국제 표준 초안(DIS) 투표와 ISO 출판 절차가 이어지는데, 정확한 출판 시점은 ISO 절차에 따라 달라지므로 공식 발표는 isocpp.org에서 확인하는 것이 좋습니다. 이 글에서 다루는 기능 자체는 기능 마감 이후 바뀌지 않는 범위의 내용입니다.
C++26은 C++11 이후 가장 변화가 큰 표준이라는 평가를 많이 받습니다. C++11이 auto, 람다, 스마트 포인터, 이동 의미론으로 일상적인 코드 작성 방식을 바꿨다면, C++26은 리플렉션, Contracts, 메모리 안전성 개선, std::execution으로 라이브러리 작성 방식과 안전성 기본값을 바꾸려는 표준입니다.
이 글에서 다루는 것
- C++26의 네 가지 핵심 기능
- 재컴파일·빌드 설정만으로 얻는 안전성 개선과 코드를 고쳐야 얻는 이점의 구분
- Contracts 표준화 과정의 논쟁
- 2026년 9월 기준 컴파일러 지원 현황
- C++29에서 논의되는 방향
C++26 네 가지 핵심 기능
정적 리플렉션
리플렉션은 프로그램이 자신의 구조(타입, 멤버, 열거자 등)를 조회하는 기능입니다. C++26의 리플렉션(P2996)은 이 조회를 모두 컴파일 타임에 하고, 그 결과로 코드를 생성합니다.
무엇이 가능해지나?
// 채택된 C++26 문법 (P2996 + template for)
#include <meta>
#include <iostream>
#include <string>
struct Point {
int x;
int y;
};
// 컴파일 타임에 구조체 정보 접근
constexpr auto ctx = std::meta::access_context::unchecked();
static_assert(std::meta::nonstatic_data_members_of(^^Point, ctx).size() == 2);
// 멤버 목록을 순회하며 직렬화 코드 생성 (산술 타입 멤버만 가정)
template<typename T>
std::string serialize(const T& obj) {
std::string result = "{";
bool first = true;
template for (constexpr auto m :
std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
if (!first) result += ", ";
first = false;
result += std::meta::identifier_of(m);
result += ": ";
result += std::to_string(obj.[:m:]);
}
return result + "}";
}
int main() {
Point p{10, 20};
std::cout << serialize(p); // {x: 10, y: 20}
}
^^Point가 타입을 std::meta::info 값으로 바꾸고, nonstatic_data_members_of가 멤버 목록을 돌려주며, template for가 멤버마다 루프 본문을 펼치고, obj.[:m:]가 그 멤버에 접근하는 코드로 바뀝니다. 초기 제안에서 쓰던 ^T 한 글자 연산자나 [:expand(...):] 형태는 채택된 문법이 아니므로, 오래된 예제를 볼 때 주의해야 합니다.
왜 중요한가?
Before (C++23): 멤버를 손으로 나열합니다.
std::string Point::to_string() const {
return "{x: " + std::to_string(x) +
", y: " + std::to_string(y) + "}";
}
// 멤버를 추가할 때마다 이 함수도 고쳐야 하고, 빠뜨려도 컴파일은 됨
After (C++26): 멤버 목록을 리플렉션으로 얻으므로, 멤버를 추가하면 serialize가 자동으로 따라갑니다.
JSON·바이너리 직렬화, DB 매핑, 디버그 출력, 열거형 ↔ 문자열 변환처럼 지금까지 매크로나 Boost.PFR, magic_enum 같은 트릭으로 해결하던 일이 표준 문법으로 가능해집니다. 문법과 예제는 C++26 리플렉션 기초에서 자세히 다룹니다.
메모리 안전성: 재컴파일과 빌드 설정으로 얻는 개선
C++26에는 코드를 고치지 않아도 효과가 있는 안전성 개선이 두 가지 들어 있습니다. 다만 둘의 성격은 다릅니다.
(1) 초기화하지 않은 변수 읽기: UB → erroneous behavior
Before (C++23):
int foo() {
int x; // 초기화하지 않음
return x; // 정의되지 않은 동작(UB)
}
UB이기 때문에 컴파일러는 “이 코드는 실행되지 않는다”고 가정하고 주변 코드를 예상 밖으로 최적화할 수 있었고, 스택에 남아 있던 이전 데이터가 새어 나가는 정보 유출 취약점의 원인이 되기도 했습니다.
C++26 (P2795):
int foo() {
int x; // 여전히 초기화하지 않은 것은 버그
return x; // erroneous behavior: 구현이 정한 어떤 값을 읽음 (0이 보장되지는 않음)
}
C++26은 이런 읽기를 erroneous behavior(잘못된 동작)라는 새 범주로 분류합니다. 동작은 정의되어 있어서 컴파일러가 이를 근거로 코드를 삭제하거나 뒤틀 수 없지만, 여전히 버그이고 구현은 이를 진단(경고, 런타임 검사)해도 됩니다. 읽히는 값이 0이라는 보장은 없습니다. 성능 때문에 의도적으로 초기화를 생략해야 하는 큰 버퍼는 [[indeterminate]] 속성으로 표시해 예전처럼 동작하게 할 수 있습니다.
(2) 표준 라이브러리 하드닝
C++26은 하드닝된 구현(hardened implementation)이라는 개념을 표준에 넣었습니다(P3471). 구현이 하드닝 모드를 제공하고 그 모드를 켜면, std::vector::operator[], std::span, std::optional::operator* 같은 연산의 일부 전제 조건이 런타임에 검사되고, 위반은 Contracts의 위반으로 처리됩니다.
std::vector<int> v = {1, 2, 3};
int x = v[10];
// 하드닝 모드 꺼짐: 기존처럼 정의되지 않은 동작
// 하드닝 모드 켜짐: 범위 검사에 걸려 계약 위반으로 처리 (예외가 아니라 보통 프로그램 종료)
주의할 점은 이것이 기본으로 켜지는 것이 아니라 구현의 모드라는 것입니다. 그리고 at()처럼 std::out_of_range 예외를 던지는 것이 아니라 계약 위반으로 처리되므로, 예외로 잡아 복구하는 용도가 아닙니다.
실제 배포 사례
표준 이전부터 벤더들은 비슷한 하드닝을 제공해 왔습니다. Google은 libc++ 하드닝 모드를 자사 서버 코드 전반에 적용한 결과를 공개했는데, 그 발표에 따르면 평균 성능 영향은 약 0.3% 수준이었고, 이 과정에서 1,000개가 넘는 버그를 찾았으며, 운영 환경의 세그멘테이션 폴트 비율이 약 30% 줄었다고 합니다(Google Security Blog, 2024년 11월). 이 수치는 Google의 코드베이스와 측정 방식에 대한 것이므로 다른 프로젝트에서 그대로 재현된다고 볼 수는 없지만, “경계 검사는 너무 비싸다”는 통념을 재검토할 근거로는 충분합니다.
Contracts: 함수 계약 명시
함수의 사전조건과 사후조건을 선언에 적고, 본문 안의 검사는 contract_assert로 쓰는 기능입니다(P2900).
기본 사용법
#include <cstddef>
// 사전조건
int divide(int a, int b)
pre(b != 0) // b는 0이 아니어야 함
{
return a / b;
}
// 사후조건: 반환값에 이름을 붙여 참조
int* allocate(std::size_t size)
pre(size > 0)
post(r: r != nullptr)
{
return new int[size];
}
// contract_assert: 본문 안의 검사
void process(int* ptr) {
contract_assert(ptr != nullptr);
// ...
}
assert와 무엇이 다른가
#include <cassert>
int divide(int a, int b) {
assert(b != 0); // NDEBUG면 사라지고, 켜져 있으면 무조건 abort
return a / b;
}
assert는 NDEBUG 하나로 켜고 끄는 매크로입니다. Contracts는 조건이 선언에 드러나고, 검사 여부와 위반 시 동작을 ignore·observe·enforce·quick_enforce 네 가지 평가 의미 중에서 구현과 빌드 설정이 고릅니다. 위반 시에는 교체 가능한 위반 핸들러가 호출되므로 진단을 한곳으로 모을 수 있습니다. 반대로 C++26 Contracts에는 사후조건에서 함수 시작 시점의 값을 참조하는 old()나 클래스 불변식 문법은 없습니다. 자세한 문법과 주의점은 C++26 Contracts 글에서 다룹니다.
표준화 과정의 논쟁
Contracts는 C++26 기능 중 표준화 과정이 가장 험난했습니다. C++20 작업 초안에 한 번 들어갔다가 출시 전에 빠졌고, 이후 최소 기능(MVP)으로 범위를 줄인 P2900이 2025년 2월 회의에서 C++26에 채택되었습니다. 채택 표결에서도 반대표가 적지 않았고, 채택 이후에도 다음과 같은 우려를 담은 문서들이 제출되었습니다.
- 검사를 켰을 때의 성능과 코드 크기
- 번역 단위마다 다른 평가 의미로 컴파일된 인라인 함수의 동작
- 가상 함수 지원 등 MVP에서 빠진 기능
결과적으로 Contracts는 C++26에 남았지만, 실무에서 쓰는 관용적인 방식이 자리 잡기까지는 C++20 이후의 Concepts처럼 몇 년이 걸릴 것으로 보는 편이 현실적입니다.
std::execution: 표준 비동기 모델
std::execution(P2300, senders/receivers)은 비동기 작업을 어디서 실행할지(scheduler)와 무엇을 할지(sender)를 분리해 조합하는 프레임워크입니다.
어떤 모습인가?
#include <execution>
// scheduler는 스레드 풀, 이벤트 루프 등 실행 컨텍스트에서 얻음
auto task = std::execution::schedule(scheduler)
| std::execution::then([]{ return fetch_data(); })
| std::execution::then([](auto data){ return process(data); })
| std::execution::then([](auto result){ save(result); });
// 완료될 때까지 현재 스레드에서 대기
std::this_thread::sync_wait(std::move(task));
장점과 현실
- 구조화된 동시성: 작업의 수명이 조합 구조 안에 묶여, 분리된(detached) 작업이 참조를 들고 사라지는 종류의 버그를 줄이기 쉬운 구조입니다. 데이터 레이스 자체를 막아 주지는 않습니다.
- 통합 인터페이스: 스레드 풀, 이벤트 루프, GPU 같은 실행 컨텍스트를 같은 방식으로 다룰 수 있게 설계되었습니다.
- 현실적인 단점: API가 크고 추상적이어서 학습 곡선이 가파르고, 표준에 들어온 것은 기반 구조 위주라 실무에서는 NVIDIA의 stdexec 같은 참조 구현이나 헬퍼 라이브러리를 함께 쓰게 됩니다.
그 밖의 변경점
위 네 가지 외에도 일상 코드에 영향을 주는 변경이 여럿 있습니다. 몇 가지만 꼽으면 다음과 같습니다.
#embed: 바이너리 파일을 컴파일 타임에 배열로 포함- 팩 인덱싱(
Ts...[0]) - 이유를 적을 수 있는
= delete("reason") - 이름 없는 자리표시자 변수
_ std::inplace_vector,std::hive같은 새 컨테이너std::simd,<linalg>같은 수치 계산 라이브러리
컴파일러 지원 현황
2026년 9월 기준 상황은 이렇습니다. 버전마다 달라지므로 실제 사용 전에는 각 컴파일러의 C++26 지원 표를 확인하세요.
- GCC 16:
-std=c++26 -freflection으로 리플렉션을 제공하고, Contracts는-fcontracts로 실험적 구현을 시험해 볼 수 있습니다. - Clang: 리플렉션은 본가 병합이 진행 중이며, Bloomberg의 clang-p2996 포크를 Compiler Explorer에서 쓸 수 있습니다. 다른 C++26 기능은 버전별로 부분 지원합니다.
- MSVC: 일부 C++26 기능을 지원하지만 리플렉션 구현은 공개되지 않았습니다.
C++26 기능을 쓰는 코드를 여러 컴파일러에서 빌드해야 한다면 __cpp_impl_reflection, __cpp_contracts 같은 기능 테스트 매크로로 분기하는 것이 안전합니다.
기존 프로젝트에서 지금 해 볼 수 있는 일
C++26 컴파일러로 옮기기 전에도 효과를 볼 수 있는 부분이 있습니다.
1. 표준 라이브러리 하드닝부터 켜 보기. 언어 모드와 상관없이 지금 쓸 수 있습니다.
# libstdc++ (GCC)
target_compile_definitions(myapp PRIVATE _GLIBCXX_ASSERTIONS)
# libc++ (Clang)
# target_compile_definitions(myapp PRIVATE _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST)
테스트와 벤치마크를 돌려 보고 성능 영향이 허용 범위라면 릴리스 빌드에도 유지할지 결정합니다.
2. assert를 Contracts 후보로 분류해 두기. 함수 첫머리의 assert는 대부분 pre, 반환 직전의 검사는 post, 나머지는 contract_assert 후보입니다. 옮기기 전에 그 조건이 정말 “버그일 때만 거짓”인지, 외부 입력 검증을 assert로 대충 해 온 것인지 구분해 두면 전환이 쉬워집니다.
3. 리플렉션으로 대체할 곳 모아 두기. 필드 목록을 여러 곳에 반복해서 적고 있는 직렬화·매핑 코드는 Boost.PFR이나 매크로로 필드 목록을 한곳에 모아 두면, 나중에 그 부분만 리플렉션으로 바꿀 수 있습니다.
C++29에서 논의되는 방향
C++26이 마무리되면서 위원회의 관심은 다음 표준으로 옮겨가고 있습니다. 아래는 이미 논의 중인 제안과 C++26에서 의도적으로 미뤄진 기능을 바탕으로 한 것으로, 확정된 계획이 아닙니다.
- 리플렉션 2단계: C++26은 구조를 읽고 그에 맞는 코드를 전개하는 데까지를 담았고, 더 일반적인 코드 생성(코드 주입)이나 더 많은 언어 요소의 리플렉션은 후속 제안으로 논의되고 있습니다.
- Contracts 확장: 사후조건에서 이전 값 참조, 가상 함수의 계약처럼 MVP에서 빠진 기능들이 후속 제안의 대상입니다.
- 안전성 프로파일(Profiles): Bjarne Stroustrup 등이 제안한 프로파일은 C++26에 들어가지 않았고, 설계 방향에 대한 논의가 계속되고 있습니다.
- 패턴 매칭: 여러 차례 제안되었지만 C++26에도 들어가지 못했고, 여전히 요청이 많은 기능입니다.
- 물리량과 단위 라이브러리: mp-units를 바탕으로 한 표준화 제안이 논의되고 있습니다.
WG21 문서는 제안일 뿐 약속이 아니고, C++26 자체도 작업 과정에서 모양이 여러 번 바뀌었습니다. “무엇을 지켜볼지” 정도로 보는 것이 좋습니다.
트러블슈팅
Contracts 검사 비용이 걱정될 때
Contracts의 런타임 비용은 기본적으로 조건식을 평가하는 비용입니다. pre(b != 0) 같은 조건은 분기 하나지만, std::is_sorted 같은 O(N) 조건을 자주 호출되는 함수에 붙이면 비용이 커집니다. 컴파일러가 알아서 검사를 제거해 주는 것이 아니라 평가 의미(ignore 등)를 빌드 설정으로 고르는 구조이므로, 비싼 조건은 경계 함수나 테스트로 옮기고 계약에는 저렴한 조건을 남기는 설계가 필요합니다.
리플렉션을 쓰면 컴파일 시간이 늘어날 때
리플렉션은 컴파일 타임에 모든 일을 하므로, 멤버가 많은 타입을 여러 번역 단위에서 반복해서 처리하면 컴파일 시간이 늘어납니다.
// 자주 쓰는 타입의 인스턴스화를 한 번역 단위로 모으기
extern template std::string serialize<Point>(const Point&);
extern template로 인스턴스화를 한곳에 모으고, <meta>처럼 무거운 헤더는 프리컴파일드 헤더에 넣는 기존 기법이 그대로 유효합니다.
std::execution이 너무 복잡할 때
자주 쓰는 패턴을 작은 헬퍼로 감싸 팀 코드에서는 그 헬퍼만 쓰게 하는 방법이 현실적입니다.
template<typename Scheduler, typename F>
auto async_run(Scheduler sch, F&& f) {
return std::execution::schedule(sch)
| std::execution::then(std::forward<F>(f));
}
마무리
핵심 요약
- 리플렉션:
^^,std::meta::info,template for로 멤버 목록을 컴파일 타임에 순회하고 코드를 생성 - 메모리 안전성: 초기화하지 않은 변수 읽기는 erroneous behavior로, 표준 라이브러리는 하드닝 모드를 표준화
- Contracts:
pre/post/contract_assert와 네 가지 평가 의미 - std::execution: scheduler와 sender를 조합하는 표준 비동기 모델
무엇부터 할까
- 재컴파일로 얻는 것: 초기화하지 않은 변수 읽기가 최적화기를 통해 더 큰 문제로 번지지 않음
- 빌드 설정으로 얻는 것: 표준 라이브러리 하드닝 (지금도 벤더 옵션으로 가능)
- 코드를 고쳐야 얻는 것: 리플렉션 기반 직렬화, Contracts, std::execution
당장 C++26 컴파일러로 옮길 수 없더라도, 하드닝을 켜서 비용을 측정하고 assert와 반복되는 필드 목록을 정리해 두는 것만으로 전환 비용을 크게 줄일 수 있습니다.
자주 묻는 질문 (FAQ)
Q. C++26에서 초기화하지 않은 변수 읽기가 UB가 아니게 되면 초기화를 생략해도 되나요?
A. 그렇지 않습니다. C++26은 초기화하지 않은 지역 변수를 읽는 것을 정의되지 않은 동작 대신 잘못된 동작(erroneous behavior)으로 분류해 컴파일러가 최적화를 이유로 코드를 엉뚱하게 바꾸는 위험을 줄였지만, 읽힌 값이 의미 있는 값이 되는 것은 아니며 여전히 버그입니다. 성능 때문에 의도적으로 초기화를 생략해야 하는 버퍼라면 [[indeterminate]] 속성으로 명시하고, 그 외의 변수는 계속 선언과 동시에 초기화하는 것이 원칙입니다.
참고 자료
- ISO C++ (isocpp.org)
- P2996: Reflection for C++26
- P2900: Contracts for C++
- P2795: Erroneous behaviour for uninitialized reads
- P3471: Standard library hardening
- P2300: std::execution
- C++ Draft Standard
같이 보면 좋은 글
- C++26 리플렉션 기초: ^^ 연산자, std::meta::info, template for로 멤버 순회하기
- C++26 Contracts: pre·post·contract_assert로 사전조건·사후조건 표현하기와 평가 의미
- C++26 프리뷰: Reflection과 신규 표준 라이브러리 제안들 [#44-1]
- C++ 컴파일 타임 리플렉션: C++26 Reflection·magic_enum·매크로 직렬화