C++20에서는 std::span을 통해 배열과 컨테이너를 더욱 효율적이고 안전하게 다룰 수 있게 되었습니다. 이번 글에서는 std::span의 개념과 사용법, 그리고 이전 버전에서의 접근 방식과 비교하여 어떻게 개선되었는지 알아보겠습니다.
std::span이란 무엇인가요?
std::span은 C++20에서 도입된 객체로, 연속적인 메모리 블록을 나타내는 뷰(view)입니다. 이는 배열이나 std::vector와 같은 컨테이너의 요소들을 복사하지 않고도 안전하게 참조할 수 있게 해줍니다. std::span은 템플릿 클래스이며, 타입과 크기를 지정할 수 있습니다.
이전 버전에서는 어떻게 했나요?
C++20 이전에는 함수에 배열이나 컨테이너를 전달할 때, 다음과 같은 방식으로 처리했습니다:
1. 포인터와 길이를 별도로 전달
void processData(int* data, std::size_t size) {
for (std::size_t i = 0; i < size; ++i) {
// data[i] 사용
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
processData(arr, 5);
return 0;
}
- 문제점:
- 포인터와 길이를 따로 전달해야 하므로, 길이 정보를 잘못 전달할 위험이 있습니다.
- 배열의 경계를 벗어난 접근으로 인한 버퍼 오버런 등의 오류가 발생할 수 있습니다.
2. 배열 전체를 참조로 전달
template <std::size_t N>
void processData(int (&data)[N]) {
for (std::size_t i = 0; i < N; ++i) {
// data[i] 사용
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
processData(arr);
return 0;
}
- 문제점:
- 템플릿을 사용하므로 배열의 크기마다 별도의 인스턴스가 생성됩니다.
- 함수 템플릿을 헤더 파일에 정의해야 하며, 코드의 복잡성이 증가합니다.
3. std::vector 또는 std::array를 전달
void processData(const std::vector<int>& data) {
for (int elem : data) {
// elem 사용
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
processData(vec);
return 0;
}
- 문제점:
- 함수에서 특정 컨테이너 타입(std::vector)에 의존하게 되어 유연성이 떨어집니다.
- 배열을 전달하려면 std::vector로 복사해야 하는 오버헤드가 발생합니다.
std::span을 사용한 개선
std::span을 사용하면 위의 문제점을 해결하고, 배열과 다양한 컨테이너를 일관된 방식으로 처리할 수 있습니다.
예제: std::span 사용
#include <span>
#include <iostream>
void processData(std::span<int> data) {
for (int elem : data) {
std::cout << elem << ' ';
}
std::cout << '\n';
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
processData(arr); // 배열 전달
std::vector<int> vec = {6, 7, 8, 9, 10};
processData(vec); // vector 전달
std::array<int, 3> arr2 = {11, 12, 13};
processData(arr2); // array 전달
return 0;
}
- std::span은 배열, std::vector, std::array 등 연속적인 메모리를 가지는 컨테이너를 모두 지원합니다.
- 데이터 복사 없이 포인터와 길이를 안전하게 캡슐화하여 전달합니다.
어떻게 좋아졌나요?
- 안전성 향상: 포인터와 길이를 별도로 전달할 때 발생할 수 있는 실수나 버그를 예방합니다.
- 유연성 증가: 함수가 특정 컨테이너 타입에 의존하지 않고, 다양한 연속적인 데이터 타입을 처리할 수 있습니다.
- 가독성 개선: 함수 인터페이스가 간결해지고, 의도가 명확하게 드러납니다.
- 오버헤드 감소: 데이터 복사가 필요 없으며, 오버헤드 없이 경량 객체로 전달됩니다.
std::span의 주요 기능
1. 부분 범위 생성
int arr[] = {1, 2, 3, 4, 5};
std::span<int> data(arr);
std::span<int> subData = data.subspan(1, 3); // 인덱스 1부터 3개 요소
// subData: {2, 3, 4}
- 기존의 배열이나 컨테이너에서 일부 요소를 참조할 수 있습니다.
- 데이터 복사 없이 부분적인 뷰를 생성합니다.
2. 고정 크기 std::span
void fixedSizeProcess(std::span<int, 3> data) {
// 정확히 3개의 요소를 가진 span만 허용
}
int main() {
int arr[3] = {7, 8, 9};
fixedSizeProcess(arr); // 올바름
int arr2[4] = {1, 2, 3, 4};
// fixedSizeProcess(arr2); // 컴파일 오류: 크기가 3이 아님
return 0;
}
- 고정 크기 std::span을 사용하여 특정 크기의 데이터만 처리할 수 있습니다.
- 컴파일 타임에 크기가 검증되므로 안전성이 높아집니다.
3. 읽기 전용 std::span
void readOnlyProcess(std::span<const int> data) {
// data[0] = 10; // 컴파일 오류: 읽기 전용
}
int main() {
int arr[] = {1, 2, 3};
readOnlyProcess(arr);
return 0;
}
- std::span<const T>를 사용하여 데이터를 읽기 전용으로 전달할 수 있습니다.
- 함수 내에서 데이터의 변경을 방지하여 부작용을 최소화합니다.
주의 사항
1. 수명 관리
- std::span은 데이터를 소유하지 않으며, 단순히 참조합니다.
- 따라서, 참조하는 데이터의 수명이 std::span보다 길어야 합니다.
std::span<int> createSpan() {
int arr[] = {1, 2, 3};
return std::span<int>(arr); // 위험: 로컬 배열의 수명 종료
}
int main() {
auto data = createSpan(); // data는 유효하지 않은 메모리를 참조
// ...
return 0;
}
- 위 예제에서 arr은 함수 종료와 함께 소멸되므로, std::span이 유효하지 않은 메모리를 참조하게 됩니다.
- 해결 방법: 데이터를 동적으로 할당하거나, 상위 스코프에서 생성하여 수명을 보장해야 합니다.
2. 연속적인 메모리 요구
- std::span은 연속적인 메모리 블록을 나타내므로, std::list와 같은 연속적이지 않은 컨테이너는 지원하지 않습니다.
std::list<int> myList = {1, 2, 3};
// std::span<int> data(myList); // 컴파일 오류
- 연속적인 메모리를 가지는 컨테이너(std::vector, std::array, C 배열 등)에만 사용할 수 있습니다.
요약
std::span은 C++20에서 도입된 강력한 기능으로, 배열과 연속적인 컨테이너를 안전하고 효율적으로 참조할 수 있게 해줍니다. 이전 버전에서는 포인터와 길이를 별도로 전달하거나, 특정 컨테이너에 의존해야 했지만, std::span을 사용하면 코드의 안전성, 유연성, 가독성을 모두 향상시킬 수 있습니다.
참고 자료:
반응형
'개발 이야기 > C++' 카테고리의 다른 글
C++26 지금까지 알려진 사실 (0) | 2024.12.06 |
---|---|
[C++20 새기능 소개] using enum (0) | 2024.12.05 |
[C++20 새기능 소개] 개선된 람다 캡처 (Lambda Capture) (0) | 2024.12.03 |
[C++20 새기능 소개] std::format 라이브러리 (32) | 2024.12.02 |
[C++20 새기능 소개] consteval과 constinit 키워드 (30) | 2024.12.01 |