C++23 핵심 기능 완벽 가이드 | std::expected·mdspan
이 글의 핵심
예외 대신 std::expected로 에러 처리, mdspan으로 다차원 배열 뷰, deducing this로 CRTP 제거, std::print로 타입 안전 출력, if consteval로 컴파일 타임 분기. C++23 실전 패턴과 자주 발생하는 에러까지.
들어가며: “예외를 쓸 수 없는데 에러를 어떻게 전달하지?”
실무에서 겪는 C++23 이전의 한계
게임 엔진은 60fps로 돌아가며, 예외가 한 번 발생하면 스택 언와인딩 비용과 예측 불가능한 지연이 생깁니다. 설정 파싱, 리소스 로딩처럼 “실패할 수 있는 정상 경로”가 많은데, 예외로 처리하면 프로파일러에 스파이크가 찍힙니다. std::optional은 “값 없음”만 표현하고 왜 실패했는지는 알 수 없습니다.
문제의 코드:
// ❌ 나쁜 예: optional은 에러 원인을 전달 못 함
#include <optional>
#include <string>
std::optional<int> parse_int(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // "왜" 실패했는지 호출자가 모름
}
}
// ❌ 나쁜 예: cout + << 는 타입 안전하지 않고 느림
std::cout << "Value: " << value << ", Error: " << err << "\n";
C++23로 해결:
// ✅ std::expected: 에러 정보까지 전달
#include <expected>
std::expected<int, std::string> parse_int(const std::string& s) {
if (s.empty()) return std::unexpected("empty string");
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(std::string("parse failed: ") + e.what());
}
}
// ✅ std::print: 타입 안전, 빠른 출력
#include <print>
std::print("Value: {}, Error: {}\n", value, err);
이 글을 읽으면:
std::expected로 예외 없이 에러를 명시적으로 전달할 수 있습니다.std::mdspan으로 2D/3D 배열을mat(i, j)처럼 직관적으로 다룰 수 있습니다.deducing this로 CRTP 없이 파생 타입 기반 로직을 구현할 수 있습니다.std::print로 타입 안전하고 빠른 출력을 할 수 있습니다.if consteval로 컴파일 타임/런타임 분기를 명확히 할 수 있습니다.- 자주 발생하는 에러와 프로덕션 패턴을 알 수 있습니다.
개념을 잡는 비유
C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.
목차
- 실무에서 겪는 문제 시나리오
- std::expected: 값 또는 에러
- std::mdspan: 다차원 배열 뷰
- deducing this: 파생 타입 자동 추론
- std::print·std::println: 타입 안전 출력
- if consteval: 컴파일 타임 분기
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 완전한 C++23 통합 예제
- C++20/17 마이그레이션 가이드
- C++23 도입 체크리스트
- 정리
1. 실무에서 겪는 문제 시나리오
시나리오 1: 게임 엔진에서 예외를 쓸 수 없는 환경
문제: 게임 루프는 60fps로 돌아가며, 예외가 발생하면 스택 언와인딩 비용과 예측 불가능한 지연이 발생합니다. 설정 파일 파싱, 리소스 로딩처럼 “실패할 수 있는 정상 경로”가 많은데, 예외로 처리하면 프로파일러에 스파이크가 찍힙니다.
해결: std::expected로 “성공 시 값, 실패 시 에러”를 반환값으로 명시적으로 전달하면 예외 비용 없이 에러 경로를 처리할 수 있습니다.
시나리오 2: 수치 연산에서 2D/3D 배열을 1D로 다루는 번거로움
문제: std::vector<double>에 64×64 행렬을 저장할 때, data[i * cols + j]처럼 인덱스를 직접 계산해야 합니다. 행/열 stride가 다른 외부 라이브러리(OpenCV, cuBLAS)와 연동할 때 레이아웃 변환이 반복됩니다.
해결: std::mdspan으로 연속 메모리를 2D/3D 뷰로 해석하면 mat(i, j)처럼 직관적으로 접근하고, 레이아웃 정책만 바꿔서 외부 API와 맞출 수 있습니다.
시나리오 3: cout << 체이닝의 가독성과 성능 문제
문제: std::cout << "x=" << x << ", y=" << y << "\n"는 타입에 따라 포맷이 달라지고, 여러 번의 << 호출로 버퍼 플러시가 반복될 수 있습니다. printf는 타입 안전하지 않습니다.
해결: C++23 std::print는 std::format 기반으로 타입 안전하고, 단일 버퍼에 포맷 후 한 번에 출력해 성능이 좋습니다.
시나리오 4: 컴파일 타임과 런타임에서 다른 구현이 필요할 때
문제: std::is_constant_evaluated()는 if constexpr와 함께 쓸 때 실수하기 쉽고, consteval 함수를 constexpr 함수 안에서 호출하려면 복잡한 조건이 필요합니다.
해결: if consteval로 “지금 컴파일 타임인가?”를 명확히 분기하면, consteval 함수 호출이 안전하게 동작합니다.
시나리오 5: CRTP로 파생 타입을 반환하는 clone()의 반복
문제: CRTP를 쓰면 Derived 타입을 템플릿 인자로 넘겨야 하고, 보일러플레이트가 많아집니다.
해결: deducing this로 this Self&& self를 쓰면 컴파일러가 호출 객체의 실제 타입을 자동 추론해, CRTP 없이 clone()이 실제 타입을 반환하도록 구현할 수 있습니다.
2. std::expected: 값 또는 에러
에러 처리의 새로운 패러다임
- 예외는 “예외적인” 상황에 쓰기 좋지만, “실패할 수 있는 정상 경로”(파싱 실패, 파일 없음 등)를 반환값으로 표현하고 싶을 때가 많습니다.
- std::expected<T, E>는 “성공하면 T, 실패하면 E”를 담는 타입입니다. std::optional이 “값 있음/없음”이라면, expected는 “값 또는 에러 정보”입니다.
flowchart TD
subgraph expected["std expectedT, E"]
A[함수 호출] --> B{성공?}
B -->|예| C[값 T 반환]
B -->|아니오| D[에러 E 반환]
C --> E["result.value()"]
D --> F["result.error()"]
end
기본 사용
#include <expected>
#include <string>
#include <iostream>
std::expected<int, std::string> parse_int(const std::string& s) {
if (s.empty()) return std::unexpected("empty string");
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("invalid number: " + s);
}
}
int main() {
auto result = parse_int("123");
if (result) {
std::cout << "Value: " << *result << "\n";
} else {
std::cerr << "Error: " << result.error() << "\n";
}
}
- value(): 값이 있으면 반환, 없으면
std::bad_expected_access예외. - error(): 에러가 있으면 E 반환.
- operator bool(): 값이 있으면 true.
- std::unexpected(e)로 에러 케이스를 반환합니다.
C++23 모나딕 연산: and_then, or_else, transform
#include <expected>
#include <string>
#include <iostream>
#include <cmath>
std::expected<int, std::string> parse_int(const std::string& s) {
if (s.empty()) return std::unexpected("empty");
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("invalid: " + s);
}
}
std::expected<double, std::string> sqrt_safe(int x) {
if (x < 0) return std::unexpected("negative");
return std::sqrt(static_cast<double>(x));
}
int main() {
// and_then: 성공 시 다음 연산, 실패 시 에러 전파
auto result = parse_int("16")
.and_then(sqrt_safe)
.transform( { return d * 2; });
if (result) {
std::cout << *result << "\n"; // 8.0
} else {
std::cerr << result.error() << "\n";
}
// or_else: 실패 시 복구 시도
auto recovered = parse_int("abc")
.or_else( {
return std::expected<int, std::string>(0);
});
}
| 연산 | 설명 |
|---|---|
| and_then(f) | 값이 있으면 f(값) 호출, f는 expected 반환. 없으면 에러 전파 |
| or_else(f) | 에러가 있으면 f(에러) 호출. 값이 있으면 그대로 |
| transform(f) | 값이 있으면 f(값) 적용, 결과 타입으로 변환 |
| transform_error(f) | 에러가 있으면 f(에러)로 에러 타입 변환 |
expected vs optional vs 예외 선택 가이드
| 상황 | 추천 방법 |
|---|---|
| 실패가 예외적이고 드문 경우 | 예외 사용 |
| 실패가 정상 흐름이고 에러 정보 필요 | std::expected |
| 실패가 정상이지만 에러 정보 불필요 | std::optional |
| 성능이 매우 중요한 경로 | std::expected (예외 비용 없음) |
실전 코드: expected로 에러 체이닝
#include <expected>
#include <string>
#include <fstream>
enum class FileError { NotFound, PermissionDenied, InvalidFormat };
std::expected<std::string, FileError> read_file(const std::string& path) {
std::ifstream file(path);
if (!file) return std::unexpected(FileError::NotFound);
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return content;
}
std::expected<int, FileError> parse_config(const std::string& path) {
auto content = read_file(path);
if (!content) return std::unexpected(content.error());
// 파싱 로직...
return 42;
}
3. std::mdspan: 다차원 배열 뷰
다차원 배열을 뷰로
- std::mdspan은 기존 메모리(연속 배열)를 다차원 뷰로 보게 해 줍니다. 소유는 하지 않고, 레이아웃(행/열, stride)만 정해 주는 뷰 타입입니다.
- std::span이 1차원이었다면, mdspan은 2차원, 3차원, 그 이상을 템플릿 인자로 차원 수와 레이아웃 정책을 받습니다.
flowchart LR
subgraph mem["연속 메모리"]
M[data: 0,1,2,3,4,5,...]
end
subgraph view["mdspan 뷰 2x3"]
V["(0,0)=0 (0,1)=1 (0,2)=2br/(1,0)=3 (1,1)=4 (1,2)=5"]
end
mem --> view
기본 사용
#include <mdspan>
#include <vector>
#include <iostream>
int main() {
const size_t rows = 3, cols = 4;
std::vector<double> data(rows * cols);
for (size_t i = 0; i < data.size(); ++i) data[i] = static_cast<double>(i);
// 2차원 뷰: 행 rows, 열 cols (layout_right: 행 우선)
std::mdspan mat(data.data(), rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
std::cout << mat(i, j) << " ";
}
std::cout << "\n";
}
}
레이아웃과 extent
#include <mdspan>
#include <vector>
// 동적 2차원 (행·열 모두 런타임)
using Matrix2D = std::mdspan<double, std::dextents<size_t, 2>>;
// 3x4 고정 크기
using FixedMatrix = std::mdspan<double, std::extents<size_t, 3, 4>>;
int main() {
std::vector<double> buf(12);
Matrix2D mat(buf.data(), 3, 4);
FixedMatrix fixed(buf.data());
}
- layout_right: C 스타일, 마지막 차원이 연속 (기본값)
- layout_left: Fortran 스타일, 첫 차원이 연속
- extents:
std::extents<size_t, 3, 4>처럼 컴파일 타임 고정 크기, 또는std::dextents<size_t, 2>처럼 동적 크기
실전 예제: 이미지 버퍼를 2D로 처리
#include <mdspan>
#include <vector>
#include <cstdint>
void process_image(std::vector<uint8_t>& pixels, size_t width, size_t height) {
// 3차원 뷰: [height][width][4] (RGBA)
std::mdspan img(pixels.data(), height, width, size_t(4));
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
uint8_t& r = img(y, x, 0);
uint8_t& g = img(y, x, 1);
uint8_t& b = img(y, x, 2);
uint8_t& a = img(y, x, 3);
uint8_t gray = static_cast<uint8_t>(0.299*r + 0.587*g + 0.114*b);
r = g = b = gray;
}
}
}
4. deducing this: 파생 타입 자동 추론
멤버 함수에서 “자기 타입” 자동 추론
- deducing this는 비정적 멤버 함수의 첫 번째 인자로 this를 명시적으로 받을 수 있게 하는 기능입니다.
- 이 인자의 타입을 템플릿으로 두면, 파생 클래스에서 호출했을 때 자동으로 파생 클래스 타입이 추론됩니다.
struct Base {
template <typename Self>
void f(this Self&& self) {
// Self는 호출 시점의 실제 타입 (Derived 등)
}
};
struct Derived : Base {};
Derived d;
d.f(); // Self = Derived
clone() 패턴: CRTP 대체
#include <memory>
struct Base {
virtual ~Base() = default;
template <typename Self>
auto clone(this Self&& self) {
return std::make_unique<std::remove_cvref_t<Self>>(
static_cast<std::remove_cvref_t<Self> const&>(self)
);
}
};
struct Derived : Base {
int value{};
};
int main() {
auto d = std::make_unique<Derived>();
d->value = 42;
auto copy = d->clone(); // std::unique_ptr<Derived> 반환 (CRTP 불필요)
}
왜 쓰는가
- CRTP 없이 파생 타입 기반 로직을 멤버 함수 하나로 표현할 수 있어, 코드가 짧아지고 의도가 분명해집니다.
강화된 람다: 패킹 캡처 (C++23)
C++23에서는 람다 캡처에 패킹을 쓸 수 있습니다. [...args=std::move(args)]처럼 init-capture에서 팩 확장이 가능합니다. ellipsis는 식별자 앞에 옵니다.
#include <utility>
#include <iostream>
template <class F, class... Args>
auto delay_invoke(F f, Args... args) {
// C++23: 패킹 init-capture
return [f = std::move(f), ...args = std::move(args)]() -> decltype(auto) {
return std::invoke(f, args...);
};
}
int add(int a, int b) { return a + b; }
int main() {
auto delayed = delay_invoke(add, 3, 5);
std::cout << delayed() << "\n"; // 8
}
C++20 이전에는 std::tuple과 std::apply를 써서 구현해야 했습니다:
// C++20 방식 (번거로움)
template <class F, class... Args>
auto delay_invoke_old(F f, Args... args) {
auto tup = std::make_tuple(std::move(args)...);
return [f = std::move(f), tup = std::move(tup)]() -> decltype(auto) {
return std::apply(f, tup);
};
}
정적 operator() (캡처 없는 람다)
캡처가 없는 람다는 static operator()를 가질 수 있습니다. 호출 시 객체를 통해 접근할 필요가 없어 최적화에 유리합니다.
// C++23: static 람다 (캡처 없음)
auto identity = static { return x; };
static_assert(identity(42) == 42);
// 캡처가 있으면 static 불가
// auto bad = [x](int i) static { return i + x; }; // 컴파일 에러
5. std::print·std::println: 타입 안전 출력
cout 대신 std::format 기반 출력
C++23에서 <print> 헤더가 추가되었습니다. std::format 문법({} 플레이스홀더)을 사용하며, 타입 안전하고 printf보다 빠릅니다.
#include <print>
#include <string>
int main() {
int x = 42;
double y = 3.14;
std::string message = "Hello";
// std::print: 줄바꿈 없음
std::print("x={}, y={:.2f}\n", x, y);
// std::println: 자동 줄바꿈
std::println("Message: {}", message);
// stderr로 출력
std::println(std::cerr, "Error: code {}", -1);
// 포맷 지정자
std::print("Hex: {:x}, Binary: {:b}\n", 255, 16);
}
std::print vs cout vs printf
| 항목 | std::print | std::cout | printf |
|---|---|---|---|
| 타입 안전 | ✅ | ✅ | ❌ |
| 포맷 문법 | {} (Python 스타일) | << | %d, %s 등 |
| 성능 | 버퍼 단일 쓰기 | 여러 << 호출 | 가변 인자 |
| C++23 | ✅ | C++98 | C |
실전 예제: 로깅
#include <print>
#include <chrono>
#include <format>
void log_info(std::string_view msg, auto&&... args) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf;
#ifdef _WIN32
localtime_s(&tm_buf, &time);
#else
localtime_r(&time, &tm_buf);
#endif
std::print("[{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}] INFO: ",
tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday,
tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec);
std::println(msg, std::forward<decltype(args)>(args)...);
}
6. if consteval: 컴파일 타임 분기
컴파일 타임 vs 런타임 명확한 분기
std::is_constant_evaluated()는 if constexpr와 함께 쓸 때 실수하기 쉽습니다. C++23 if consteval은 “지금 컴파일 타임인가?”를 명확히 분기합니다.
#include <cmath>
// 컴파일 타임에는 constexpr 버전, 런타임에는 std::sqrt 사용
double sqrt_impl(double x) {
if consteval {
// 컴파일 타임: constexpr sqrt (C++26에서 가능할 수 있음)
return x; // 또는 컴파일 타임 전용 구현
} else {
return std::sqrt(x);
}
}
// consteval 함수 호출이 필요한 경우
consteval int compile_time_only() { return 42; }
int wrapper(int x) {
if consteval {
return compile_time_only(); // consteval 안에서만 호출 가능
} else {
return x;
}
}
if !consteval
void process() {
if !consteval {
// 런타임에서만 실행
std::print("Running at runtime\n");
} else {
// 컴파일 타임에서만 실행
// 컴파일 타임 전용 로직
}
}
std::is_constant_evaluated() vs if consteval
| 항목 | std::is_constant_evaluated() | if consteval |
|---|---|---|
| if constexpr과 혼용 | 실수 가능 | 분기 명확 |
| consteval 함수 호출 | 제한적 | 안전 |
| C++ 버전 | C++20 | C++23 |
7. 자주 발생하는 에러와 해결법
std::expected 관련
에러 1: value() 호출 시 bad_expected_access
원인: 에러가 담긴 expected에서 value()를 호출하면 예외가 발생합니다.
// ❌ 잘못된 사용
auto result = parse_int("");
int x = result.value(); // std::bad_expected_access 발생!
해결:
// ✅ 올바른 사용: 먼저 성공 여부 확인
if (result) {
int x = result.value();
} else {
handle_error(result.error());
}
에러 2: and_then에 void 반환 함수 전달
원인: and_then에 전달하는 함수는 std::expected를 반환해야 합니다.
// ❌ 잘못된 사용
result.and_then( { std::cout << x; }); // void 반환, 컴파일 에러
해결:
// ✅ transform 사용 (값 변환만 할 때)
result.transform( {
std::cout << x;
return x;
});
// ✅ and_then은 expected 반환 함수에 사용
result.and_then( -> std::expected<int, Error> {
if (x < 0) return std::unexpected(Error::Negative);
return x;
});
std::mdspan 관련
에러 3: 범위 초과 접근
원인: mdspan은 bounds checking을 기본으로 하지 않습니다. 잘못된 인덱스로 접근하면 미정의 동작입니다.
// ❌ 위험한 사용
std::mdspan mat(data.data(), 3, 4);
double x = mat(5, 5); // 미정의 동작!
해결:
// ✅ 인덱스 검증
if (i < mat.extent(0) && j < mat.extent(1)) {
double x = mat(i, j);
}
에러 4: 뷰 수명 문제
원인: mdspan은 메모리를 소유하지 않습니다. 원본 버퍼가 파괴된 뒤 뷰를 사용하면 dangling reference입니다.
// ❌ 잘못된 사용
std::mdspan<double, std::dextents<size_t, 2>> get_view() {
std::vector<double> data(100);
return std::mdspan(data.data(), 10, 10); // data가 파괴되면 dangling!
}
해결:
// ✅ 버퍼와 뷰를 함께 반환
std::pair<std::vector<double>, std::mdspan<double, std::dextents<size_t, 2>>>
get_view() {
std::vector<double> data(100);
auto view = std::mdspan(data.data(), 10, 10);
return {std::move(data), view};
}
std::print 관련
에러 5: 헤더 누락
원인: std::print는 #include <print>가 필요합니다.
// ❌ 잘못된 사용
#include <iostream>
std::print("x={}\n", 42); // 컴파일 에러: std::print 선언 없음
해결:
// ✅ <print> 헤더 포함
#include <print>
std::print("x={}\n", 42);
에러 6: 포맷 문자열과 인자 불일치
원인: {} 개수와 인자 개수가 맞지 않으면 예외가 발생합니다.
// ❌ 잘못된 사용
std::print("x={}, y={}\n", 42); // 인자 부족
해결:
// ✅ 인자 개수 일치
std::print("x={}, y={}\n", 42, 3.14);
if consteval 관련
에러 7: if consteval에서 non-constexpr 코드
원인: if consteval 블록 안에는 컴파일 타임에 평가 가능한 코드만 올 수 있습니다.
// ❌ 잘못된 사용
int f(int x) {
if consteval {
std::cout << x; // I/O는 컴파일 타임 불가
}
}
해결:
// ✅ 컴파일 타임에 가능한 코드만
int f(int x) {
if consteval {
return x * 2; // constexpr 가능
} else {
std::cout << x;
return x;
}
}
람다 패킹 캡처 관련
에러 8: ellipsis 위치 오류
원인: init-capture pack에서 ellipsis는 식별자 앞에 와야 합니다.
// ❌ 잘못된 사용
// [args... = std::move(args)] // 컴파일 에러
// ✅ C++23 올바른 문법
[...args = std::move(args)]
8. 베스트 프랙티스
std::expected
- 에러 타입은 가벼우면서 정보량 있게:
std::string보다 enum + 메시지 조합이 성능에 유리할 수 있습니다. - value() 호출 전 반드시 검사:
if (result)또는result.has_value()로 확인합니다. - 모나딕 연산으로 체이닝:
and_then,transform으로 에러 전파를 일관되게 합니다.
std::mdspan
- 버퍼 수명 관리: 뷰가 참조하는 버퍼가 뷰보다 먼저 파괴되지 않도록 합니다.
- 인덱스 검증: 디버그 빌드에서는
extent()로 범위를 검사합니다. - 레이아웃 명시: 외부 API와 연동 시
layout_right/layout_left를 명확히 합니다.
std::print
- 로깅에 std::println 활용: 자동 줄바꿈으로 코드가 깔끔해집니다.
- 에러는 stderr로:
std::println(std::cerr, ...)로 표준 에러에 출력합니다. - 포맷 지정자 활용:
{:.2f},{:x}등으로 출력 형식을 제어합니다.
if consteval
- 컴파일 타임 전용 로직 분리: consteval 함수 호출이 필요한 경우
if consteval블록 안에서만 호출합니다. - std::is_constant_evaluated 대체: C++23에서는
if consteval를 우선 사용합니다.
9. 프로덕션 패턴
패턴 1: Result 타입 별칭으로 API 일관성
struct AppError {
int code;
std::string message;
};
template <typename T>
using Result = std::expected<T, AppError>;
Result<std::string> load_config(const std::string& path);
Result<Image> load_texture(const std::string& path);
패턴 2: expected와 로깅 연동
template <typename T, typename E>
auto log_on_error(std::expected<T, E> r, std::string_view context) {
if (!r) {
std::println(std::cerr, "{}: {}", context, r.error());
}
return r;
}
auto result = log_on_error(parse_config(path), "config load");
패턴 3: mdspan으로 다중 레이아웃 뷰
std::vector<double> buf(rows * cols);
std::mdspan<double, std::dextents<size_t, 2>, std::layout_right> row_major(
buf.data(), rows, cols);
std::mdspan<double, std::dextents<size_t, 2>, std::layout_left> col_major(
buf.data(), rows, cols);
패턴 4: deducing this로 CRTP 제거
// Before: CRTP
template <typename Derived>
struct BaseCRTP {
void doit() { static_cast<Derived*>(this)->impl(); }
};
// After: deducing this
struct Base {
template <typename Self>
void doit(this Self&& self) {
std::forward<Self>(self).impl();
}
};
패턴 5: 모나딕 체이닝으로 파이프라인 구성
auto pipeline = read_file(path)
.and_then(parse_json)
.transform(parse_config)
.or_else( {
return std::expected<Config, Error>(default_config());
});
10. 완전한 C++23 통합 예제
설정 로더 + 이미지 처리 + 로깅을 C++23 기능으로 통합한 예제입니다.
#include <expected>
#include <mdspan>
#include <print>
#include <vector>
#include <string>
#include <fstream>
#include <memory>
enum class ConfigError { FileNotFound, ParseError };
struct Config { size_t width = 640, height = 480; };
std::expected<Config, ConfigError> load_config(const std::string& path) {
std::ifstream f(path);
if (!f) return std::unexpected(ConfigError::FileNotFound);
Config c;
f >> c.width >> c.height;
return c;
}
void brighten_image(std::vector<uint8_t>& pixels, size_t width, size_t height) {
std::mdspan img(pixels.data(), height, width);
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x)
img(y, x) = static_cast<uint8_t>(std::min(255, img(y, x) + 30));
}
struct ImageProcessor {
template <typename Self>
auto clone(this Self&& self) {
return std::make_unique<std::remove_cvref_t<Self>>(
static_cast<std::remove_cvref_t<Self> const&>(self));
}
};
template <class F, class... Args>
auto defer(F f, Args... args) {
return [f = std::move(f), ...args = std::move(args)]() mutable {
return std::invoke(f, args...);
};
}
int main() {
auto config = load_config("config.txt");
if (!config) {
std::println(std::cerr, "Config error: {}", static_cast<int>(config.error()));
return 1;
}
std::vector<uint8_t> pixels(config->width * config->height, 128);
brighten_image(pixels, config->width, config->height);
std::print("Processed {}x{} image\n", config->width, config->height);
auto task = defer(brighten_image, std::ref(pixels), config->width, config->height);
task();
}
C++20/17에서 C++23 마이그레이션 가이드
체크리스트
| 단계 | 작업 | 비고 |
|---|---|---|
| 1 | 컴파일러 버전 확인 | GCC 13+, Clang 17+, MSVC 2022 17.6+ |
| 2 | 빌드 플래그 변경 | -std=c++23 또는 /std:c++latest |
| 3 | expected 마이그레이션 | optional + 에러 코드 → expected<T, E> |
| 4 | mdspan 도입 | 2D 인덱스 계산 [i*cols+j] → mat(i,j) |
| 5 | std::print 도입 | cout/printf → std::print/std::println |
| 6 | 람다 패킹 | tuple+apply → [...args=std::move(args)] |
| 7 | CRTP 제거 | deducing this로 clone() 등 단순화 |
optional → expected 마이그레이션 예시
// C++20: optional + 별도 에러 로깅
std::optional<int> parse_old(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
log_error("parse failed");
return std::nullopt;
}
}
// C++23: expected로 에러 정보까지 전달
std::expected<int, std::string> parse_new(const std::string& s) {
try {
return std::stoi(s);
} catch (const std::exception& e) {
return std::unexpected(std::string("parse failed: ") + e.what());
}
}
2D 배열 → mdspan 마이그레이션 예시
// C++20: 수동 인덱스 계산
void process_old(std::vector<double>& data, size_t rows, size_t cols) {
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
data[i * cols + j] *= 2;
}
}
}
// C++23: mdspan으로 직관적 접근
void process_new(std::vector<double>& data, size_t rows, size_t cols) {
std::mdspan mat(data.data(), rows, cols);
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
mat(i, j) *= 2;
}
}
}
cout → std::print 마이그레이션 예시
// C++20: cout
std::cout << "Value: " << value << ", Error: " << err << "\n";
// C++23: std::print
std::print("Value: {}, Error: {}\n", value, err);
12. C++23 도입 체크리스트
- 컴파일러: GCC 13+, Clang 17+, MSVC 2022 17.6+ 확인
- 빌드 플래그:
-std=c++23또는/std:c++latest설정 - expected 도입: 기존
optional+ 에러 코드 조합을expected<T, E>로 교체 검토 - mdspan 도입: 2D/3D 인덱스 계산이 많은 모듈에 적용
- std::print 도입: 로깅·디버그 출력을
std::print/std::println로 전환 - if consteval: 컴파일 타임/런타임 분기가 필요한 코드에 적용
- deducing this: CRTP 사용처를
this Self&&패턴으로 마이그레이션 검토 - 테스트: 기존 단위 테스트가 C++23 빌드에서 통과하는지 확인
- CI/CD: 모든 타겟 플랫폼에서 C++23 지원 여부 확인
13. 정리
| 기능 | 요약 |
|---|---|
| std::expected<T, E> | “값 T 또는 에러 E” — 예외 대안, 명시적 에러 전달, 모나딕 연산 |
| std::mdspan | 연속 메모리를 다차원 뷰로 — 행렬·이미지 등 |
| deducing this | 멤버 함수에서 실제 객체 타입 자동 추론 — CRTP 단순화 |
| std::print·println | 타입 안전·빠른 출력 — std::format 기반 |
| if consteval | 컴파일 타임/런타임 분기 — consteval 함수 호출 안전 |
| 람다 강화 | 패킹 캡처 [...args=std::move(args)], static operator() |
C++23은 에러 처리(expected), 다차원 데이터(mdspan), 표현력(deducing this, 람다), 출력(std::print), 컴파일 타임(if consteval)을 한꺼번에 올려 주는 표준입니다.
컴파일러 지원 현황 (2026년 3월 기준)
- GCC 13+: std::expected, mdspan, std::print, if consteval 지원.
-std=c++23플래그 사용. - Clang 17+: expected, mdspan, std::print, if consteval 지원.
- MSVC 2022 (17.6+): expected, mdspan, std::print 지원.
/std:c++latest플래그.
요약 표: 기능별 적용 시점
| 기능 | 도입 우선순위 | 예상 효과 |
|---|---|---|
| std::expected | 높음 | 에러 처리 명확화, 예외 비용 제거 |
| std::print | 높음 | 로깅 가독성·성능 향상 |
| std::mdspan | 중간 | 2D/3D 코드 가독성 향상 |
| if consteval | 중간 | 컴파일 타임 최적화 명확화 |
| deducing this | 낮음 | CRTP 제거, clone 등 패턴 단순화 |
자주 묻는 질문 (FAQ)
Q. std::expected와 std::optional을 같이 쓸 수 있나요?
A. 네. expected<T, std::monostate>는 “에러는 있지만 상세 정보 없음”을 표현해 optional과 유사합니다. 에러 정보가 필요하면 expected<T, Error> 형태가 더 적합합니다.
Q. mdspan은 std::span과 어떤 관계인가요?
A. std::span은 1차원 연속 시퀀스 뷰이고, std::mdspan은 2차원 이상의 다차원 뷰입니다. 1차원만 필요하면 span이 더 단순합니다.
Q. std::print가 cout보다 빠른가요?
A. std::print는 내부적으로 std::format으로 버퍼에 포맷한 뒤 한 번에 출력하므로, 여러 번의 << 호출보다 효율적일 수 있습니다. 구현체에 따라 다릅니다.
Q. if consteval과 std::is_constant_evaluated()의 차이는?
A. if consteval은 분기가 명확하고, consteval 함수를 안전하게 호출할 수 있습니다. std::is_constant_evaluated()는 if constexpr와 혼용 시 의도와 다르게 동작할 수 있습니다.
한 줄 요약: C++23의 expected·mdspan·deducing this·std::print·if consteval로 최신 표현력을 쓸 수 있습니다. 다음으로 클린 코드(#38-1)를 읽어보면 좋습니다.
이전 글: [C++ GUI #36-2] 크로스 플랫폼 GUI: Qt 기초
다음 글: [C++ 아키텍처 #38-1] C++ 클린 코드 기초: const, noexcept, [[nodiscard]]
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ optional·variant·any | “nullptr 체크 지겹다” C++17 타입 안전 처리
- C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지
- C++ std::string_view·std::span 완벽 가이드 | 제로카피 뷰·댕글링 방지
이 글에서 다루는 키워드 (관련 검색어)
C++, C++23, std::expected, std::mdspan, deducing this, std::print, if consteval, 최신표준, 에러처리, 다차원배열 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++26 핵심 기능 완벽 가이드 | 리플렉션 ^^· std::execution
- C++ 컨테이너 기반 개발: Docker로 빌드 환경 표준화 및 배포 이미지 최적화 [#40-3]
- C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]