C++20 std::span과 C++23 std::mdspan — 비소유 배열 뷰
std::span은 C++20에서 도입된 비소유(non-owning) 연속 메모리 뷰입니다. 배열, std::vector, std::array 등 다양한 컨테이너를 복사 없이 참조할 수 있습니다. C++23의 std::mdspan은 이를 다차원으로 확장합니다.
1. std::span 기본
섹션 제목: “1. std::span 기본”#include <span>#include <vector>#include <iostream>
void print_elements(std::span<const int> s) { for (int v : s) std::cout << v << ' '; std::cout << '\n';}
int main() { std::vector<int> v = {1, 2, 3, 4, 5}; int arr[] = {10, 20, 30};
print_elements(v); // vector → span print_elements(arr); // C 배열 → span print_elements({v.data(), 3}); // 처음 3개만}2. 정적 익스텐트 vs 동적 익스텐트
섹션 제목: “2. 정적 익스텐트 vs 동적 익스텐트”#include <span>
// 동적 익스텐트 (크기를 런타임에 결정)std::span<int> dynamic_span;
// 정적 익스텐트 (크기를 컴파일 타임에 고정)std::span<int, 4> static_span; // 항상 4개
int arr[4] = {1, 2, 3, 4};std::span<int, 4> s = arr; // OK// std::span<int, 3> s2 = arr; // 컴파일 오류: 크기 불일치3. 부분 뷰 — subspan, first, last
섹션 제목: “3. 부분 뷰 — subspan, first, last”#include <span>#include <array>
std::array<int, 8> data = {0,1,2,3,4,5,6,7};std::span<int> view = data;
auto front3 = view.first(3); // {0,1,2}auto back3 = view.last(3); // {5,6,7}auto middle = view.subspan(2, 4); // {2,3,4,5}4. 바이트 뷰 — as_bytes / as_writable_bytes
섹션 제목: “4. 바이트 뷰 — as_bytes / as_writable_bytes”#include <span>#include <cstddef>
void serialize(std::span<const std::byte> bytes) { // 바이트 단위 직렬화}
float f = 3.14f;auto bytes = std::as_bytes(std::span{&f, 1});serialize(bytes);5. C++23 std::mdspan — 다차원 배열 뷰
섹션 제목: “5. C++23 std::mdspan — 다차원 배열 뷰”#include <mdspan>#include <vector>
int main() { std::vector<double> data(6);
// 2×3 행렬로 해석 std::mdspan<double, std::extents<std::size_t, 2, 3>> mat(data.data());
mat[0, 0] = 1.0; mat[0, 1] = 2.0; mat[1, 2] = 6.0;}6. 동적 extent mdspan
섹션 제목: “6. 동적 extent mdspan”#include <mdspan>#include <vector>
void matrix_multiply( std::mdspan<const double, std::dextents<std::size_t, 2>> A, std::mdspan<const double, std::dextents<std::size_t, 2>> B, std::mdspan<double, std::dextents<std::size_t, 2>> C){ for (std::size_t i = 0; i < A.extent(0); ++i) for (std::size_t j = 0; j < B.extent(1); ++j) for (std::size_t k = 0; k < A.extent(1); ++k) C[i, j] += A[i, k] * B[k, j];}7. span vs pointer+size 비교
섹션 제목: “7. span vs pointer+size 비교”| 항목 | T* + size_t | std::span<T> |
|---|---|---|
| 타입 안전 | ✗ | ✓ |
| 범위 검사 | 없음 | 디버그 모드 |
| STL 호환 | 어려움 | ✓ (ranges) |
| 오버헤드 | 없음 | 없음 (포인터+크기) |
8. mdspan 레이아웃 정책
섹션 제목: “8. mdspan 레이아웃 정책”#include <mdspan>#include <vector>
std::vector<double> data(12);std::iota(data.begin(), data.end(), 0.0); // 0~11
// 행 우선 (C-style, 기본값) — 같은 행이 연속using row_major = std::layout_right;std::mdspan<double, std::extents<int, 3, 4>, row_major> A(data.data());
// 열 우선 (Fortran-style) — 같은 열이 연속using col_major = std::layout_left;std::mdspan<double, std::extents<int, 3, 4>, col_major> B(data.data());
// stride 레이아웃 — 임의 보폭 (슬라이스, 하위 행렬 접근)std::array<std::size_t, 2> strides = {4, 1}; // 행 보폭=4, 열 보폭=1std::layout_stride::mapping mapping(std::extents<int,3,4>{}, strides);std::mdspan<double, std::extents<int,3,4>, std::layout_stride> C(data.data(), mapping);9. 실전 패턴 — span 안전하게 사용하기
섹션 제목: “9. 실전 패턴 — span 안전하게 사용하기”#include <span>#include <cassert>
// 수명 주의: span은 참조이므로 원본이 살아있어야 함std::span<int> dangerous() { std::vector<int> local = {1, 2, 3}; return local; // 댕글링 포인터! local은 여기서 소멸}
// 안전한 패턴: span을 매개변수로만 사용void process(std::span<const int> data) { assert(!data.empty()); // 빈 span 방어 for (int v : data) { /* 처리 */ }}
// span을 반환할 때는 외부 버퍼만std::span<int> get_first_half(std::span<int> data) { return data.first(data.size() / 2); // 원본 버퍼의 뷰}
// API 설계: 읽기 전용은 const spanvoid read_only(std::span<const float> weights);// 수정 가능은 비const spanvoid normalize(std::span<float> weights);std::span은 함수 인터페이스에서 배열 종류에 상관없이 연속 메모리를 받을 때 표준 방법입니다. T*와 size_t 쌍을 받는 기존 인터페이스를 std::span으로 교체하면 타입 안전성과 가독성이 동시에 향상됩니다. std::mdspan은 수치 계산이나 이미지 처리처럼 다차원 배열을 다룰 때 포인터 산술 없이 안전한 접근을 제공합니다.