C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]

C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]

이 글의 핵심

C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]에 대해 정리한 개발 블로그 글입니다. 22~25번 시리즈에서 C++20을 다뤘다면, 한 발 더 나아가 C++23의 핵심만 골라 "남들보다 먼저 써보는" 느낌으로 정리하면, 'C++ 최신 트렌드'를 검색할 때 블로그가 상단에 노출되기 좋습니다. 이 글은… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성…

들어가며: C++20 다음은? — C++23으로 한 발 더

최신 표준을 선점하는 글

22~25번 시리즈에서 C++20을 다뤘다면, 한 발 더 나아가 C++23의 핵심만 골라 “남들보다 먼저 써보는” 느낌으로 정리하면, ‘C++ 최신 트렌드’를 검색할 때 블로그가 상단에 노출되기 좋습니다.
이 글은 std::expected(값 또는 에러를 담는 타입—예외 대신 반환값으로 실패를 표현), std::mdspan(다차원 메모리를 배열처럼 보는 뷰—연속 메모리를 2차원·3차원으로 해석), 강화된 람다deducing this를 중심으로 C++23을 맛보는 내용입니다. 컴파일러 지원이 되는 환경에서 바로 시험해 볼 수 있는 수준으로 썼습니다.

이 글에서 다루는 것:

  • std::expected: “값 또는 에러”를 담는 타입 — 예외 대안
  • std::mdspan: 다차원 배열을 로 다루기
  • 람다 강화: 패킹 선언, 정적 연산자(), 속성 등
  • deducing this: 멤버 함수에서 파생 클래스 타입 자동 추론

개념을 잡는 비유

C++에서 자주 쓰는 비유로, 템플릿은 붕어빵 틀, 스마트 포인터는 자동 청소 로봇, RAII는 자동문처럼 스코프를 나가며 자원을 정리한다고 기억해 두면 다른 글과도 연결하기 쉽습니다.


목차

  1. std::expected: 값 또는 에러
  2. std::mdspan: 다차원 배열 뷰
  3. 강화된 람다
  4. deducing this
  5. 자주 발생하는 에러와 해결법
  6. C++20/17에서 C++23 마이그레이션 가이드
  7. 프로덕션 패턴
  8. 완전한 C++23 통합 예제
  9. 성능 고려사항
  10. C++23 도입 체크리스트
  11. 정리

문제 시나리오: C++23이 필요한 순간

시나리오 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: 가변 인자로 함수를 지연 호출할 때의 보일러플레이트

문제: std::tuplestd::apply를 써서 람다로 인자 팩을 캡처하려면 코드가 길어지고 가독성이 떨어집니다.

해결: C++23 람다 패킹 캡처 [...args=std::move(args)]로 한 줄에 깔끔하게 표현할 수 있습니다.

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

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

해결: deducing thisthis Self&& self를 쓰면 컴파일러가 호출 객체의 실제 타입을 자동 추론해, CRTP 없이 clone()이 실제 타입을 반환하도록 구현할 수 있습니다.


1. 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

기본 사용

std::expected<T, E> 는 성공 시 T 값을, 실패 시 E(에러 정보)를 담습니다. return std::unexpected(”…”) 로 에러를 반환하고, 호출부에서는 if (result) 로 성공 여부를 보고, 성공이면 *result 또는 result.value(), 실패면 result.error() 로 에러를 꺼냅니다. 예외를 쓰지 않아도 에러 경로가 명시적으로 드러나고, optional 과 달리 “왜 실패했는지”까지 전달할 수 있습니다.

#include <expected>
#include <string>

std::expected<int, std::string> parse_int(const std::string& s) {
    if (s.empty()) return std::unexpected("empty string");
    // 파싱 시도...
    if (/* 실패 */) return std::unexpected("invalid number");
    return 42;
}

int main() {
    auto result = parse_int("123");
    if (result) {
        int value = *result;  // 42
    } else {
        std::string err = result.error();  // 에러 메시지
    }
}
  • value(): 값이 있으면 반환, 없으면 예외 또는 std::bad_expected_access.
  • error(): 에러가 있으면 E 반환.
  • operator bool(): 값이 있으면 true.
  • std::unexpected(e) 로 에러 케이스를 반환합니다.

C++23 모나딕 연산: and_then, or_else, transform

C++23에서는 std::expected모나딕 연산이 추가되어, 에러 체크 없이 연산을 체이닝할 수 있습니다.

#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(에러)로 에러 타입 변환

왜 쓰는가

  • 예외 비용을 피하면서 에러 정보를 호출자에게 넘기고 싶을 때.
  • API 계약이 “성공 시 T, 실패 시 E”로 명확해지고, 호출부에서 if (result) 패턴으로 일관되게 처리할 수 있습니다.

expected vs optional vs 예외 선택 가이드

상황추천 방법
실패가 예외적이고 드문 경우예외 사용
실패가 정상 흐름이고 에러 정보 필요std::expected
실패가 정상이지만 에러 정보 불필요std::optional
성능이 매우 중요한 경로std::expected (예외 비용 없음)

실무 예시: 파일 파싱은 실패가 흔하므로 expected<Data, ParseError>, 설정 값 조회는 없을 수 있으므로 optional<string>, 메모리 할당 실패는 드물고 복구 불가능하므로 예외를 던지는 것이 적합합니다.

실전 코드: expected로 에러 체이닝

#include <expected>
#include <string>
#include <fstream>

enum class FileError { NotFound, PermissionDenied, InvalidFormat };

std::expected<std::string, FileError> readFile(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> parseConfig(const std::string& path) {
    auto content = readFile(path);
    if (!content) return std::unexpected(content.error());  // 에러 전파

    // 파싱 로직...
    return 42;
}

int main() {
    auto result = parseConfig("config.txt");
    if (result) {
        std::cout << "Value: " << *result << "\n";
    } else {
        switch (result.error()) {
            case FileError::NotFound:
                std::cerr << "File not found\n"; break;
            case FileError::PermissionDenied:
                std::cerr << "Permission denied\n"; break;
            case FileError::InvalidFormat:
                std::cerr << "Invalid format\n"; break;
        }
    }
}

이 패턴은 예외 없이도 에러를 명시적으로 전파하고 처리할 수 있어, 임베디드나 게임처럼 예외를 쓰기 어려운 환경에서 유용합니다.

다음 단계로 나아가기

이 글을 마스터했다면:

  • C++26 프리뷰: 다음 표준의 제안들 미리 보기
  • Reflection: 컴파일 타임 타입 정보 활용
  • Pattern Matching: 더 강력한 분기 문법

관련 글: optional·variant(#12-3), 에러 처리(#8-1)


2. 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

기본 사용

std::mdspan 은 이미 할당된 연속 메모리(예: vectordata())를 다차원 뷰로 해석합니다. data.data(), rows, cols 를 넘기면 mat(i, j) 로 (i, j) 원소에 접근할 수 있고, 메모리는 복사하지 않습니다. 행렬·이미지 버퍼를 span 처럼 “소유 없이 보기만” 할 때 쓰면 됩니다.

#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);

    // mat(i, j)로 (i,j) 원소 접근
    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

  • layout_right: C 스타일, 마지막 차원이 연속 (기본값)
  • layout_left: Fortran 스타일, 첫 차원이 연속
  • extents: std::extents<size_t, 3, 4>처럼 컴파일 타임 고정 크기, 또는 std::dextents<size_t, 2>처럼 동적 크기
#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());  // 크기 이미 지정됨
}

실전 예제: 이미지 버퍼를 2D로 처리

#include <mdspan>
#include <vector>
#include <cstdint>

// RGBA 이미지: height x width x 4
void process_image(std::vector<uint8_t>& pixels, size_t width, size_t height) {
    // 3차원 뷰: [height][width][4]
    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;
        }
    }
}

3. 강화된 람다

패킹 선언 (Lambda pack capture)

C++23에서는 람다 캡처패킹을 쓸 수 있습니다. [...xs=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 람다 (캡처 없음) — static은 파라미터 뒤, 본문 앞
auto identity =  static { return x; };
static_assert(identity(42) == 42);

// 캡처가 있으면 static 불가
// auto bad = [x](int i) static { return i + x; };  // 컴파일 에러

속성

람다에 [[nodiscard]]속성을 붙일 수 있습니다.

auto get_value =  [[nodiscard]] { return 42; };
// get_value();  // 경고: 반환값 무시
int x = get_value();  // OK

4. deducing this

멤버 함수에서 “자기 타입” 자동 추론

  • deducing this비정적 멤버 함수첫 번째 인자this명시적으로 받을 수 있게 하는 기능입니다.
  • 이 인자의 타입을 템플릿으로 두면, 파생 클래스에서 호출했을 때 자동으로 파생 클래스 타입이 추론됩니다.

this Self&& self 에서 Self 는 컴파일러가 호출 객체의 실제 타입으로 채웁니다. Derived d; d.f(); 이면 SelfDerived 가 되어, CRTP처럼 파생 타입을 템플릿 인자로 넘기지 않아도 멤버 함수 안에서 Self 로 파생 타입을 쓸 수 있습니다. clone() 이 실제 타입을 반환하는 패턴을 짧게 쓸 때 유용합니다.

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을 쓴다면 deducing this부터 시험해 보기 좋습니다.

5. 자주 발생하는 에러와 해결법

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);  // 미정의 동작!

해결:

// ✅ 인덱스 검증 또는 std::mdspan에 default_accessor 대신
//    bounds-checking accessor 사용 (구현에 따라 다름)
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() {
    auto data = std::make_shared<std::vector<double>>(100);
    auto view = std::mdspan(data->data(), 10, 10);
    return {*data, view};  // 수명 관리 주의
}

람다 패킹 캡처 관련

에러 5: ellipsis 위치 오류

원인: init-capture pack에서 ellipsis는 식별자 앞에 와야 합니다. C++20에서는 pack init-capture가 없어 std::tuple을 썼지만, C++23에서는 ...이 식별자 앞에 옵니다.

// ❌ 잘못된 사용: ...이 식별자 뒤에 오면 pack이 아님
// [args... = std::move(args)]  // 컴파일 에러

// ✅ C++23 올바른 문법: ...이 식별자 앞
[...args = std::move(args)]

deducing this 관련

에러 6: 캡처 람다에서 deducing this와 무관한 타입 사용

원인: 캡처가 있는 람다에서 this std::any처럼 람다 타입과 무관한 explicit object parameter를 쓰면 컴파일 에러가 납니다.

// ❌ 잘못된 사용 (캡처 람다 + 무관한 타입)
int x = 0;
auto f = [x](this std::any self) { };  // 컴파일 에러

해결: 캡처 람다에서는 explicit object parameter가 람다 자신의 타입이어야 합니다. 비캡처 람다에서만 임의 타입 사용이 가능합니다.

에러 7: mdspan에 nullptr 전달

원인: mdspan 생성 시 datanullptr이면, 이후 operator() 호출 시 미정의 동작이 발생합니다.

// ❌ 잘못된 사용
std::vector<double> empty;
std::mdspan mat(empty.data(), 0, 0);  // empty가 비어 있으면 data()가 nullptr
mat(0, 0);  // 미정의 동작

해결: extent가 0이 아닌 경우 반드시 유효한 버퍼를 전달하고, 빈 뷰가 필요하면 extent를 0으로 두어 접근 자체를 막습니다.

에러 8: expected의 에러 타입으로 예외 불가 타입 사용

원인: std::expectedvalue()는 에러가 있을 때 std::bad_expected_access<E>를 던질 수 있는데, E가 예외로 던져질 수 없는 타입(예: std::error_code의 복사 생성자가 noexcept)이면 문제가 될 수 있습니다. 일반적으로는 Estd::string이나 enum으로 두면 무방합니다.


6. 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)
5람다 패킹tuple+apply[...args=std::move(args)]
6CRTP 제거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;
        }
    }
}

7. 프로덕션 패턴

패턴 1: Result 타입 별칭으로 API 일관성

// 프로젝트 전역 에러 타입
struct AppError {
    int code;
    std::string message;
};

template <typename T>
using Result = std::expected<T, AppError>;

Result<std::string> loadConfig(const std::string& path);
Result<Image> loadTexture(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) {
        spdlog::error("{}: {}", context, r.error());
    }
    return r;
}

auto result = log_on_error(parseConfig(path), "config load");

패턴 3: mdspan으로 다중 레이아웃 뷰

// 같은 메모리를 행 우선 vs 열 우선으로 해석
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);
// 외부 API 요구사항에 맞게 선택

패턴 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());
    });

완전한 C++23 통합 예제: 설정 로더 + 이미지 처리

아래 예제는 std::expected, std::mdspan, 람다 패킹, deducing this를 한 프로젝트에서 함께 사용하는 통합 시나리오입니다.

#include <expected>
#include <mdspan>
#include <vector>
#include <string>
#include <fstream>
#include <memory>

// 1. expected로 설정 로딩
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;
    // 간단한 파싱 (실제로는 JSON 등 사용)
    f >> c.width >> c.height;
    return c;
}

// 2. mdspan으로 이미지 버퍼 2D 뷰
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));
}

// 3. deducing this로 다형적 clone
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));
    }
};

struct BrightnessProcessor : ImageProcessor {};

// 4. 람다 패킹으로 지연 실행
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) return 1;

    std::vector<uint8_t> pixels(config->width * config->height, 128);
    brighten_image(pixels, config->width, config->height);

    auto task = defer(brighten_image, std::ref(pixels), config->width, config->height);
    task();  // 나중에 실행
}

성능 고려사항

std::expected vs 예외

항목std::expected예외
성공 경로 비용거의 없음 (분기 1회)없음
실패 경로 비용O(1), 에러 값 복사스택 언와인딩, 예측 어려움
인라인용이복잡
적합 환경게임, 임베디드, 고빈도 API일반 애플리케이션

요약: “실패가 정상인” 경로(파싱, IO, 검증)에서는 expected가 예외보다 예측 가능하고 비용이 낮습니다.

std::mdspan 오버헤드

  • mdspan 자체는 포인터 + extent 정보만 갖는 얇은 뷰입니다. mat(i, j) 접근은 data[i * stride + j]와 동일한 인덱스 계산으로 컴파일됩니다.
  • 레이아웃이 layout_right(기본)이면 C 스타일 행 우선과 동일해, 캐시 친화적입니다.

C++23 도입 체크리스트

실무에서 C++23 기능을 도입할 때 참고할 체크리스트입니다.

  • 컴파일러: GCC 13+, Clang 17+, MSVC 2022 17.6+ 확인
  • 빌드 플래그: -std=c++23 또는 /std:c++latest 설정
  • expected 도입: 기존 optional + 에러 코드 조합을 expected<T, E>로 교체 검토
  • mdspan 도입: 2D/3D 인덱스 계산이 많은 모듈에 적용
  • 람다 패킹: tuple+apply 패턴을 [...args=std::move(args)]로 단순화
  • deducing this: CRTP 사용처를 this Self&& 패턴으로 마이그레이션 검토
  • 테스트: 기존 단위 테스트가 C++23 빌드에서 통과하는지 확인
  • CI/CD: 모든 타겟 플랫폼에서 C++23 지원 여부 확인

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++23 핵심 기능 완벽 가이드 | std::expected·mdspan
  • C++ expected | “에러 처리” 가이드
  • C++ Optional 완벽 가이드 | nullopt·value_or·C++23 모나딕 연산·성능·실전 패턴

이 글에서 다루는 키워드 (관련 검색어)

C++23 새 기능, C++23 튜토리얼, std::expected 사용법, mdspan 예제, deducing this, C++23 표준, 최신 C++ 기능, C++ 에러 처리, 다차원 배열 C++, C++23 람다 등으로 검색하시면 이 글이 도움이 됩니다.

8. 정리

기능요약
std::expected<T, E>“값 T 또는 에러 E” — 예외 대안, 명시적 에러 전달, 모나딕 연산
std::mdspan연속 메모리를 다차원 로 — 행렬·이미지 등
람다 강화패킹 캡처, static operator(), 속성 지원
deducing this멤버 함수에서 실제 객체 타입 자동 추론 — CRTP 단순화

C++23은 에러 처리(expected), 다차원 데이터(mdspan), 표현력(람다, deducing this) 를 한꺼번에 올려 주는 표준입니다. 컴파일러가 C++23 모드를 지원하면 위 기능부터 차례로 써 보면 “최신 C++” 트렌드를 선점할 수 있습니다.

컴파일러 지원 현황 (2026년 3월 기준)

  • GCC 13+: std::expected, mdspan 대부분 지원. -std=c++23 플래그 사용.
  • Clang 17+: expected, mdspan 지원. -std=c++2b 또는 -std=c++23.
  • MSVC 2022 (17.6+): expected, mdspan 지원. /std:c++latest 플래그.

실무에서는 컴파일러 버전을 확인하고, 지원되는 기능부터 점진적으로 도입하는 것이 안전합니다.

요약 표: 기능별 적용 시점

기능도입 우선순위예상 효과
std::expected높음에러 처리 명확화, 예외 비용 제거
std::mdspan중간2D/3D 코드 가독성 향상
람다 패킹중간지연 호출·래퍼 코드 단순화
deducing this낮음CRTP 제거, clone 등 패턴 단순화

시리즈 35~37 여기까지가 현대 C++ 실무 융합(Python/Wasm), GUI(ImGui/Qt), C++23 프리뷰입니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 에러 처리의 새 패러다임 std::expected, 다차원 배열 뷰 mdspan, 더 강력해진 람다와 deducing this. C++23 핵심을 한 번에 정리합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

Q. std::expected와 std::optional을 같이 쓸 수 있나요?

A. 네. expected<T, E>에서 Estd::monostate로 두면 “에러는 있지만 상세 정보 없음”을 표현할 수 있어, optional과 비슷한 용도로 쓰이기도 합니다. 다만 에러 정보가 필요하면 expected<T, Error> 형태가 더 적합합니다.

Q. mdspan은 std::span과 어떤 관계인가요?

A. std::span은 1차원 연속 시퀀스 뷰이고, std::mdspan은 2차원 이상의 다차원 뷰입니다. mdspanextents<size_t, N>처럼 1차원을 주면 span과 유사하게 쓸 수 있지만, 1차원만 필요하면 span이 더 단순합니다.

한 줄 요약: C++23의 expected·mdspan·deducing this 등으로 최신 표현력을 쓸 수 있습니다. 다음으로 클린 코드(#38-1)를 읽어보면 좋습니다.

이전 글: [C++ GUI #36-2] 크로스 플랫폼 GUI: Qt 기초 찍어먹기

다음 글: [C++ 아키텍처 #38-1] C++ 클린 코드 기초: const, noexcept, [[nodiscard]]로 의도 명확히 하기


관련 글

  • C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리
  • C++ 파일 연산 완벽 가이드 | ifstream·바이너리 I/O·mmap·io_uring·원자적 쓰기까지
  • C++23 핵심 기능 완벽 가이드 | std::expected·mdspan
  • C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
  • C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]