C++23 핵심 기능 완벽 가이드 | std::expected·mdspan

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는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.


목차

  1. 실무에서 겪는 문제 시나리오
  2. std::expected: 값 또는 에러
  3. std::mdspan: 다차원 배열 뷰
  4. deducing this: 파생 타입 자동 추론
  5. std::print·std::println: 타입 안전 출력
  6. if consteval: 컴파일 타임 분기
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 완전한 C++23 통합 예제
  11. C++20/17 마이그레이션 가이드
  12. C++23 도입 체크리스트
  13. 정리

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::printstd::format 기반으로 타입 안전하고, 단일 버퍼에 포맷 후 한 번에 출력해 성능이 좋습니다.

시나리오 4: 컴파일 타임과 런타임에서 다른 구현이 필요할 때

문제: std::is_constant_evaluated()if constexpr와 함께 쓸 때 실수하기 쉽고, consteval 함수를 constexpr 함수 안에서 호출하려면 복잡한 조건이 필요합니다.

해결: if consteval로 “지금 컴파일 타임인가?”를 명확히 분기하면, consteval 함수 호출이 안전하게 동작합니다.

시나리오 5: CRTP로 파생 타입을 반환하는 clone()의 반복

문제: CRTP를 쓰면 Derived 타입을 템플릿 인자로 넘겨야 하고, 보일러플레이트가 많아집니다.

해결: deducing thisthis 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::tuplestd::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::printstd::coutprintf
타입 안전
포맷 문법{} (Python 스타일)<<%d, %s
성능버퍼 단일 쓰기여러 << 호출가변 인자
C++23C++98C

실전 예제: 로깅

#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++20C++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

  1. 에러 타입은 가벼우면서 정보량 있게: std::string보다 enum + 메시지 조합이 성능에 유리할 수 있습니다.
  2. value() 호출 전 반드시 검사: if (result) 또는 result.has_value()로 확인합니다.
  3. 모나딕 연산으로 체이닝: and_then, transform으로 에러 전파를 일관되게 합니다.

std::mdspan

  1. 버퍼 수명 관리: 뷰가 참조하는 버퍼가 뷰보다 먼저 파괴되지 않도록 합니다.
  2. 인덱스 검증: 디버그 빌드에서는 extent()로 범위를 검사합니다.
  3. 레이아웃 명시: 외부 API와 연동 시 layout_right/layout_left를 명확히 합니다.

std::print

  1. 로깅에 std::println 활용: 자동 줄바꿈으로 코드가 깔끔해집니다.
  2. 에러는 stderr로: std::println(std::cerr, ...)로 표준 에러에 출력합니다.
  3. 포맷 지정자 활용: {:.2f}, {:x} 등으로 출력 형식을 제어합니다.

if consteval

  1. 컴파일 타임 전용 로직 분리: consteval 함수 호출이 필요한 경우 if consteval 블록 안에서만 호출합니다.
  2. 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
3expected 마이그레이션optional + 에러 코드 → expected<T, E>
4mdspan 도입2D 인덱스 계산 [i*cols+j]mat(i,j)
5std::print 도입cout/printfstd::print/std::println
6람다 패킹tuple+apply[...args=std::move(args)]
7CRTP 제거deducing thisclone() 등 단순화

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]