std::span
std::span 이란
C++의 데이터 컨테이너를 메모리 상의 저장 방식의 차이로 나눠보자면, 크게 연속적인 컨테이너(Sequential Type)과 비연속적인 컨테이너(Non sequential Type)로 나눌 수 있습니다. 연속적인 컨테이너는 C-스타일 배열, std::array
, std::vector
, std::string
등이 그 예시입니다. 반면 비연속적인 컨테이너로는 std::list
, std::map
, std::unordered_map
, std::set
, std::unordered_set
, std::stack
등이 있습니다. 이러한 컨테이너는 주로 내부적으로 포인터를 통해 연결되어 있으며, 그렇기 때문에 메모리 구조상에서 서로 떨어져 있습니다.
std::span
연속적인 데이터 컨테이너를 참조하는 STL입니다. C++17부터 도입됐습니다.
std::span
은 컴파일 단계에서 크기가 결정된 경우 정적 길이를 가지며, 실행 시간에 그 크기가 결정된다면 동적 길이(dynamic_extent)를 가집니다.
std::span 의 사용
std::span
은 다음과 같이 사용할 수 있습니다.
template <typename T>
void print(const std::span<T> &span) {
for (const auto &i : span) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main(void) {
std::vector<int> vecNums{1,2,3};
std::array<int, 3> arrStrs{4,5,6};
int arrNums[] = {7,8,9};
std::string str = "Hello World!";
print(vecNums);
print(arrStrs);
print(arrNums);
print(str);
return 0;
}
출력:
1 2 3
4 5 6
7 8 9
H e l l o W o r l d !
std::span, 왜 사용하는걸까?
std::span은 얼핏 보기에 왜 사용하는 지 그 이유를 알기 어렵습니다. 하나의 함수에 벡터, 스트링, 배열 등을 동시에 담을 수 있게 하려는 것일까요? 그것도 맞는 말이지만, 이미 C++에는 템플릿이라는 강력한 기능이 있고, template concept라는 아주 강력한 기능에 힘을 얻어 C++20부터 더 안전한 템플릿의 사용이 가능해졌습니다. 하지만 std::span은 다음과 같은 주요한 기능이 있습니다.
- 안전성(Safety):
std::span
은 범위 확인을 제공하고 버퍼 오버플로우를 방지합니다. 즉 일반 C-array나 포인터를 사용하는 것보다 더 안전합니다. - 쉬운 사용성(Easy to use):
std::span
은 배열 등 여러 연속적인 메모리 컨테이너를 다루는데 사용하기 편리한 메소드들과 인터페이스를 제공합니다. - 상호 운용성(Interoperability):
std::span
은 여러 연속적인 메모리 컨테이너와 배열과 함께 사용될 수 있습니다. 그렇기 때문에 여러 STL의 알고리즘들과 함께 사용될 수 있습니다. - 성능(Performance):
std::span
은 일반 어레이를 사용하는 것보다 살짝 더 빠르고 안전하게 메모리 블럭을 참조할 수 있게 설계됐습니다.
각각의 예시들에 대해서 간단히 설명해보죠.
안전성(Safety)
std::span
은 위에 예시에서 범위 기반 for-loop에서 알 수 있듯이 범위를 제공합니다. 그 size()
메소드를 제공하기에 그 크기를 알 수 있고, C++20부터 begin()
, end()
와 같은 iterator들도 제공합니다.
따라서 다음과 같은 C 스타일 배열이나 포인터의 사용을 개선할 수 있습니다.
void print(const int* arr, const int size)
{
for (int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int main(void)
{
int arr[] = { 1, 2, 3, 4, 5 };
print(arr, sizeof(arr) / sizeof(int));
return 0;
}
위 함수는 반드시 배열의 크기를 인자로 받아야 했지만, std::span을 사용한다면 그럴 필요가 없습니다.
쉬운 사용성(Easy to use)
std::span
은 여러 편리한 인터페이스를 제공합니다. 몇 가지를 소개하자면
size
: span하는 대상의 사이즈를 반환합니다.begin
,end
: span하는 메모리의 각각 첫 번째, 마지막 + 1 번 째의 iterator를 반환합니다. (C++20)rbegin
,rend
:begin
,end
과 반대(reverse)로 각각 마지막, 첫 번째 - 1 번 째의 iterator를 반환합니다. (C++20)front
,back
: 각각 첫 번째, 마지막 번 째 데이터를 참조합니다.front()
는*begin()
과 결과가 똑같습니다. (C++20)subspan
:span
의 하위 범위를 가져옵니다.subspan
의 사용 예시는 다음과 같습니다(출처: Microsoft Learn | span ):
#include <algorithm>
#include <cstdio>
#include <numeric>
#include <ranges>
#include <span>
void display(std::span<const char> abc)
{
const auto columns{ 20U };
const auto rows{ abc.size() - columns + 1 };
for (auto offset{ 0U }; offset < rows; ++offset) {
std::ranges::for_each(
abc.subspan(offset, columns),
std::putchar
);
std::putchar('\n');
}
}
int main()
{
char abc[26];
std::iota(std::begin(abc), std::end(abc), 'A');
display(abc);
}
출력:
mySpan.subspan(1,2): 12
mySpan.subspan<1,2>: 12
mySpan.subspan<1>: 12
또는 다음과 같이 쓸 수도 있습니다(출처: std::span cppreference)
#include
#include
#include
#include
#include
void display(std::span abc)
{
const auto columns{ 20U };
const auto rows{ abc.size() - columns + 1 };
for (auto offset{ 0U }; offset < rows; ++offset) {
std::ranges::for_each(
abc.subspan(offset, columns),
std::putchar
);
std::putchar('\n');
}
}
int main()
{
char abc[26];
std::iota(std::begin(abc), std::end(abc), 'A');
display(abc);
}
결과:
ABCDEFGHIJKLMNOPQRST
BCDEFGHIJKLMNOPQRSTU
CDEFGHIJKLMNOPQRSTUV
DEFGHIJKLMNOPQRSTUVW
EFGHIJKLMNOPQRSTUVWX
FGHIJKLMNOPQRSTUVWXY
GHIJKLMNOPQRSTUVWXYZ
- 그 외에 C++20부터
dynamic_extent
를 통해 동적 길이를 가지는 span을 생성할 수 있습니다. - 또한 C++23부터
std::mdspan
을 통해 다차원 배열에 대한 span을 생성할 수 있습니다.
더 많은 기능들과 예시를 살펴보고 싶다면, std::span cppreference나 Microsoft Learn을 참고하십시오. 공식 문서를 읽는 것은 언제나 큰 도움이 됩니다.
상호 운용성(Interoperability)
std::span
은 여러 연속적인 메모리 컨테이너와 함께 사용될 수 있기에, 코드의 재사용성을 줄이는 한편 여러 STL의 알고리즘들 혹은 함수들과 같이 사용될 수 있습니다.
심지어 직접 제작한 컨테이너에도 std::span
을 적용할 수 있는데, 다음 조건만 만족하면 됩니다.
- 데이터가 연속된 메모리 공간에 저장될 것
- 데이터 포인터와 길이를 가질 것
예시:
template <typename T>
class MyContainer {
public:
MyContainer(std::vector<T> data) : data_(data) {}
T* data() { return data_.data(); }
std::size_t size() { return data_.size(); }
private:
std::vector<T> data_;
};
int main() {
MyContainer<int> c = {{1, 2, 3, 4, 5}};
std::span<int> s(c.data(), c.size());
for (const auto &i : s) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
출력:
1 2 3 4 5
결론
std::span
은 매우 유용한 컨테이너 도구입니다. C++17부터 소개되었고, 소개될 때도 그렇게까지 강한 파급력은 없었지만, 몇몇 상황에서는 유용하게 쓰일 수 있는 기능입니다.