C++ span 심화 | "배열 뷰" C++20 가이드

C++ span 심화 | "배열 뷰" C++20 가이드

이 글의 핵심

C++ span 심화에 대한 실전 가이드입니다.

들어가며

**std::span**은 C++20에서 도입된 배열 뷰입니다. 배열이나 vector의 연속 메모리를 복사 없이 참조하며, string_view와 유사한 개념입니다.


1. span 기본

기본 사용

#include <span>
#include <vector>
#include <iostream>

// std::span<int>: int 배열의 뷰 (복사 없이 참조)
// 배열, vector, 다른 연속 메모리 컨테이너 모두 받을 수 있음
void print(std::span<int> s) {
    // span은 범위 기반 for문 지원
    // 내부적으로 포인터와 크기를 가짐
    for (int x : s) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
}

int main() {
    // 배열: 자동으로 span으로 변환
    // 크기 정보가 자동으로 전달됨 (배열 decay 방지)
    int arr[] = {1, 2, 3, 4, 5};
    print(arr);
    
    // vector: 자동으로 span으로 변환
    // vector의 연속 메모리를 참조 (복사 없음)
    std::vector<int> v = {6, 7, 8, 9, 10};
    print(v);
    
    // 부분 배열: 명시적으로 span 생성
    // std::span(포인터, 크기): 배열의 일부만 뷰로 전달
    print(std::span(arr, 3));  // 처음 3개만: 1 2 3
    
    return 0;
}

출력:

1 2 3 4 5
6 7 8 9 10
1 2 3

동적 vs 정적 크기

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    
    // 동적 크기 (런타임): std::span<int>
    // 크기가 템플릿 인자에 없음 → 런타임에 크기 저장
    // 장점: 유연함 (다양한 크기 배열 받음)
    // 단점: 크기 정보가 런타임에 저장됨 (약간의 오버헤드)
    std::span<int> dynamicSpan(arr);
    std::cout << "동적 크기: " << dynamicSpan.size() << std::endl;  // 5
    
    // 정적 크기 (컴파일 타임): std::span<int, 5>
    // 크기가 템플릿 인자로 명시 → 컴파일 타임에 크기 확정
    // 장점: 크기 검증 (컴파일 타임), 최적화 가능
    // 단점: 정확한 크기만 받을 수 있음
    std::span<int, 5> staticSpan(arr);
    std::cout << "정적 크기: " << staticSpan.size() << std::endl;  // 5
    
    // 크기 불일치 시 컴파일 에러
    // std::span<int, 10> wrongSpan(arr);  // ❌ 에러: 배열은 5개인데 10개 요구
    
    return 0;
}

2. subspan

부분 범위

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::span<int> s(arr);  // 전체 배열의 뷰
    
    // subspan(offset, count): 부분 범위 추출
    // offset: 시작 인덱스 (0부터)
    // count: 요소 개수
    
    // 처음 3개: 인덱스 0부터 3개
    auto first3 = s.subspan(0, 3);  // [1, 2, 3]
    std::cout << "처음 3개: ";
    for (int x : first3) std::cout << x << " ";
    std::cout << std::endl;
    
    // 3번째부터 끝까지: count 생략 시 끝까지
    auto from3 = s.subspan(3);  // [4, 5, 6, 7, 8, 9, 10]
    std::cout << "3번째부터: ";
    for (int x : from3) std::cout << x << " ";
    std::cout << std::endl;
    
    // 중간 범위: 인덱스 3부터 4개
    auto middle = s.subspan(3, 4);  // [4, 5, 6, 7]
    std::cout << "중간 4개: ";
    for (int x : middle) std::cout << x << " ";
    std::cout << std::endl;
    
    // 모든 subspan은 원본 배열을 참조 (복사 없음)
    
    return 0;
}

출력:

처음 3개: 1 2 3
3번째부터: 4 5 6 7 8 9 10
중간 4개: 4 5 6 7

first, last

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> s(arr);
    
    // 처음 N개
    auto first2 = s.first(2);
    std::cout << "처음 2개: ";
    for (int x : first2) std::cout << x << " ";
    std::cout << std::endl;
    
    // 마지막 N개
    auto last2 = s.last(2);
    std::cout << "마지막 2개: ";
    for (int x : last2) std::cout << x << " ";
    std::cout << std::endl;
    
    return 0;
}

출력:

처음 2개: 1 2
마지막 2개: 4 5

3. const span

읽기 전용

#include <span>
#include <vector>
#include <iostream>

void read(std::span<const int> s) {
    for (int x : s) {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    // s[0] = 10;  // 컴파일 에러
}

void write(std::span<int> s) {
    for (int& x : s) {
        x *= 2;
    }
}

int main() {
    std::vector<int> v = {1, 2, 3};
    
    read(v);   // OK
    write(v);  // OK
    
    std::cout << "수정 후: ";
    read(v);   // 2 4 6
    
    const std::vector<int> cv = {4, 5, 6};
    read(cv);   // OK
    // write(cv);  // 컴파일 에러
    
    return 0;
}

4. 실전 예제

예제 1: 행렬 연산

#include <span>
#include <vector>
#include <iostream>

class Matrix {
    std::vector<double> data;
    size_t rows, cols;
    
public:
    Matrix(size_t r, size_t c) : data(r * c), rows(r), cols(c) {}
    
    // 행 접근 (수정 가능)
    std::span<double> getRow(size_t row) {
        return std::span(data.data() + row * cols, cols);
    }
    
    // 행 접근 (읽기 전용)
    std::span<const double> getRow(size_t row) const {
        return std::span(data.data() + row * cols, cols);
    }
    
    // 요소 접근
    double& operator()(size_t i, size_t j) {
        return data[i * cols + j];
    }
    
    void print() const {
        for (size_t i = 0; i < rows; ++i) {
            auto row = getRow(i);
            for (double x : row) {
                std::cout << x << " ";
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    Matrix m(3, 3);
    
    // 행 설정
    auto row0 = m.getRow(0);
    for (size_t i = 0; i < row0.size(); ++i) {
        row0[i] = i + 1;
    }
    
    auto row1 = m.getRow(1);
    for (size_t i = 0; i < row1.size(); ++i) {
        row1[i] = (i + 1) * 10;
    }
    
    auto row2 = m.getRow(2);
    for (size_t i = 0; i < row2.size(); ++i) {
        row2[i] = (i + 1) * 100;
    }
    
    // 출력
    m.print();
    
    return 0;
}

출력:

1 2 3
10 20 30
100 200 300

예제 2: 슬라이딩 윈도우

#include <span>
#include <vector>
#include <iostream>

double average(std::span<const int> window) {
    double sum = 0;
    for (int x : window) {
        sum += x;
    }
    return sum / window.size();
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    const int windowSize = 3;
    
    std::cout << "슬라이딩 윈도우 평균:" << std::endl;
    for (size_t i = 0; i <= data.size() - windowSize; ++i) {
        std::span<const int> window(data.data() + i, windowSize);
        std::cout << "윈도우 [" << i << "-" << (i + windowSize - 1) 
                  << "]: " << average(window) << std::endl;
    }
    
    return 0;
}

출력:

슬라이딩 윈도우 평균:
윈도우 [0-2]: 2
윈도우 [1-3]: 3
윈도우 [2-4]: 4
윈도우 [3-5]: 5
윈도우 [4-6]: 6
윈도우 [5-7]: 7
윈도우 [6-8]: 8
윈도우 [7-9]: 9

5. 자주 발생하는 문제

문제 1: 댕글링 span

#include <span>
#include <vector>
#include <iostream>

// ❌ 위험: 지역 변수 반환
std::span<int> dangling() {
    std::vector<int> v = {1, 2, 3};
    return v;  // v 소멸, span은 댕글링!
}

// ✅ vector 반환
std::vector<int> safe() {
    return {1, 2, 3};
}

// ✅ span 매개변수로만 사용
void process(std::span<int> s) {
    for (int x : s) {
        std::cout << x << " ";
    }
}

int main() {
    // ❌ 위험
    // auto s = dangling();
    // for (int x : s) {}  // 정의되지 않은 동작
    
    // ✅ 안전
    std::vector<int> v = safe();
    process(v);
    
    return 0;
}

문제 2: 임시 객체

#include <span>
#include <vector>
#include <iostream>

void func(std::span<int> s) {
    for (int x : s) {
        std::cout << x << " ";
    }
}

int main() {
    // ❌ 위험: 임시 객체
    // func(std::vector<int>{1, 2, 3});  // 임시 객체 소멸, 댕글링!
    
    // ✅ 변수 저장
    std::vector<int> v = {1, 2, 3};
    func(v);
    
    return 0;
}

문제 3: 크기 변경 불가

#include <span>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3};
    std::span<int> s(v);
    
    // ❌ 크기 변경 불가
    // s.push_back(4);  // 컴파일 에러
    // s.resize(10);    // 컴파일 에러
    
    // ✅ vector로 크기 변경
    v.push_back(4);
    
    // span은 자동 업데이트 안됨 (재생성 필요)
    s = std::span<int>(v);
    
    for (int x : s) std::cout << x << " ";  // 1 2 3 4
    std::cout << std::endl;
    
    return 0;
}

문제 4: 포인터 vs span

#include <span>
#include <iostream>

// ❌ 구식: 포인터 + 크기
void oldStyle(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        std::cout << data[i] << " ";
    }
}

// ✅ 현대적: span
void modernStyle(std::span<int> data) {
    for (int x : data) {
        std::cout << x << " ";
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    
    oldStyle(arr, 5);
    std::cout << std::endl;
    
    modernStyle(arr);
    std::cout << std::endl;
    
    return 0;
}

6. 실전 예제: 이미지 처리

#include <span>
#include <vector>
#include <iostream>

struct Image {
    std::vector<uint8_t> pixels;
    int width, height;
    
    Image(int w, int h) : pixels(w * h * 3), width(w), height(h) {}
    
    // 픽셀 접근 (RGB)
    std::span<uint8_t> getPixel(int x, int y) {
        int offset = (y * width + x) * 3;
        return std::span(pixels.data() + offset, 3);
    }
    
    // 행 접근
    std::span<uint8_t> getRow(int y) {
        int offset = y * width * 3;
        return std::span(pixels.data() + offset, width * 3);
    }
    
    // 그레이스케일 변환
    void toGrayscale() {
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                auto pixel = getPixel(x, y);
                uint8_t gray = static_cast<uint8_t>(
                    0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2]
                );
                pixel[0] = pixel[1] = pixel[2] = gray;
            }
        }
    }
};

int main() {
    Image img(100, 100);
    
    // 픽셀 설정 (빨강)
    auto pixel = img.getPixel(50, 50);
    pixel[0] = 255;  // R
    pixel[1] = 0;    // G
    pixel[2] = 0;    // B
    
    std::cout << "픽셀 (50, 50): RGB(" 
              << static_cast<int>(pixel[0]) << ", "
              << static_cast<int>(pixel[1]) << ", "
              << static_cast<int>(pixel[2]) << ")" << std::endl;
    
    // 그레이스케일 변환
    img.toGrayscale();
    
    auto grayPixel = img.getPixel(50, 50);
    std::cout << "그레이스케일: RGB(" 
              << static_cast<int>(grayPixel[0]) << ", "
              << static_cast<int>(grayPixel[1]) << ", "
              << static_cast<int>(grayPixel[2]) << ")" << std::endl;
    
    return 0;
}

출력:

픽셀 (50, 50): RGB(255, 0, 0)
그레이스케일: RGB(76, 76, 76)

정리

핵심 요약

  1. span: C++20 배열 뷰
  2. 복사 없음: 참조만 (소유 안함)
  3. 동적/정적 크기: 런타임/컴파일 타임
  4. subspan: 부분 범위 추출
  5. const span: 읽기 전용
  6. 수명 주의: 원본 배열 유효 동안만

span vs 다른 타입

타입소유권크기 변경타입용도
span없음불가모든 타입배열 뷰
vector있음가능모든 타입동적 배열
string_view없음불가문자열문자열 뷰
포인터+크기없음불가모든 타입구식

실전 팁

사용 원칙:

  • 배열/vector 매개변수는 span
  • 부분 배열 전달은 subspan
  • 읽기 전용은 const span
  • 소유권 필요하면 vector

성능:

  • 복사 없음 (포인터+크기만)
  • 큰 배열일수록 효과 큼
  • 인라인 최적화 가능
  • 오버헤드 거의 없음

주의사항:

  • 원본 배열 수명 관리
  • 임시 객체 주의
  • 크기 변경 불가
  • 댕글링 span 방지

다음 단계

  • C++ string_view
  • C++ vector
  • C++ Iterator

관련 글

  • C++ span 기초 |
  • C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
  • C++ Ranges Views와 파이프라인 | 지연 연산으로 효율적으로 다루기 [#25-2]
  • 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리
  • C++ Barrier & Latch |