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)
정리
핵심 요약
- span: C++20 배열 뷰
- 복사 없음: 참조만 (소유 안함)
- 동적/정적 크기: 런타임/컴파일 타임
- subspan: 부분 범위 추출
- const span: 읽기 전용
- 수명 주의: 원본 배열 유효 동안만
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 |