과거 C 스타일로 배열의 내용을 출력하려면 보통 배열 포인터와 크기를 함께 매개변수로 전달하는 방식이 사용되었다.
void Print(const int value[], size_t count)
{
for (size_t i = 0; i < count; i++)
cout << value[i] << endl;
}
이 방식은 간단하지만, 항상 배열의 크기를 함께 전달해야 하기 때문에 실수가 발생하기 쉽고, 안전하지도 않다.
다음 단계로는 vector를 매개변수로 받는 방식이 있다. 이 방식은 컨테이너 내부에 크기 정보가 포함되어 있어 더 안전하다.
void Print(const vector<int>& values)
{
for (const auto& value : values)
cout << value << endl;
}
하지만 만약 array나 C 스타일 배열도 출력하고 싶다면, 또 다른 오버로드 함수를 작성해야 한다.
이처럼 컨테이너마다 따로 Print 함수를 정의해야 하는 구조는 중복이 많고 번거롭다.
이 문제를 해결하기 위해 C++20에서는 std::span이 새롭게 도입되었다.
void Print(std::span<int> values)
{
for (const auto& value : values)
cout << value << endl;
}
위 코드처럼 표현한다면 C스타일 배열, vector, array, string등 많은 버전과 함께 사용할 수 있다.
template<class T, size_t Extent = std::dynamic_extent>
class span;
std::span의 구조에서 눈여겨볼 부분은 바로 두 번째 템플릿 인자인 Extent이다.
이 Extent는 span이 참조할 수 있는 데이터 범위의 크기를 나타내며, 두 가지 방식으로 나뉜다.
기본값은 std::dynamic_extent이며, 이는 내부적으로 -1로 정의되어 있다.
이 값은 크기를 컴파일 타임에 정하지 않고, 런타임에 결정한다는 의미다.
일반적으로 vector나 C 스타일 배열처럼 크기가 유동적인 자료구조에 사용할 수 있다.
vector<int> myVec{ 1,2,3,4,5 };
std::span<int> dynamicSpan(myVec);
이런 형태의 span은 크기를 유연하게 다룰 수 있어서 범용적인 함수에 적합하다.
Extent에 명시적으로 정수 값을 지정하면, 해당 span은 고정된 크기(static_extent)를 갖는다.
이 경우 크기는 컴파일 타임에 결정되며, 최적화가 용이하고 타입 안전성도 더 강해진다.
vector<int> myVec2{ 6,7,8,9,10 };
std::span<int, 5> staticSpan(myVec2);
이처럼 Extent를 고정하면, 해당 span은 정확히 그 크기만큼의 요소만 다루게 된다.
여기서 한 가지 주의할 점은 static_extent는 이미 정해진 값이고 dynamic_extent는 값이 변할 수 있다.
그래서 static_extent을 dynamic_extent으로 값을 대입하는 것은 가능하지만 반대의 경우는 불가능하다.
dynamicSpan = staticSpan; // O
staticSpan = dynamicSpan; // X
이는 타입 시스템 차원에서 고정 크기를 갖는 span의 안전성을 보장하기 위한 설계이다.
정해진 크기보다 더 작거나 큰 값을 실수로 대입하는 것을 컴파일 타임에 방지할 수 있다.
std::span은 다양한 컨테이너 타입과 호환되면서도 STL에서 자주 쓰이는 유용한 메서드들을 제공한다.
예를 들어 begin(), end(), data(), size() 같은 메서드를 그대로 사용할 수 있다.
또한 span은 내부 데이터를 읽고 쓸 수 있는 뷰(View) 로 동작한다는 점에서, 단순한 참조 이상의 역할을 한다.
이러한 특성 때문에 span은 다양한 컨테이너를 통합된 방식으로 처리할 수 있는 매우 실용적인 도구가 된다.
자세한 내용은 다음 코드 예시를 보자.
template<typename T>
void Print(std::span<T> container)
{
for (int i = 0; i < container.size(); i++)
cout << container[i] << " ";
cout << endl;
}
int main()
{
vector<int> myVec{ 1,2,3,4,5 };
std::span<int> span1(myVec.data(), myVec.size());
Print<int>(span1);
}
결과 : 1 2 3 4 5
span은 첫 번째 인자에 담을 첫 번째 원소의 주소를 주어지게 하고 두 번째 인자에는 개수를 전달하면 된다.
그리고 std::span을 통해 vector의 데이터를 복사 없이 참조할 수 있다.
subspan( ) 메서드를 사용하면 기존 span에서 일부분만 잘라낸 새로운 뷰를 만들 수 있다.
첫 번째 인자는 시작 위치(offset), 두 번째 인자는 길이(count)다.
std::span<int> span2(span1.subspan(1, span1.size() - 3));
Print<int>(span2);
결과 : 2 3
인덱스는 1부터 시작하여 2개의 원소를 잘라낸 뷰가 생성된다.
원본 vector의 일부에 대한 비복사 뷰이므로 효율적이다.
span은 참조하는 원본 데이터를 직접 수정할 수 있다.
이를 활용해 STL의 std::transform을 이용하면, span을 통해 원본 데이터를 일괄적으로 수정할 수 있다.
std::transform(span1.begin(), span1.end(), span1.begin(), [](int i) { return i * i; });
Print<int>(span1);
Print<int>(myVec); // 원본 데이터가 수정됨
결과: 1 4 9 16 25
1 4 9 16 25
span1을 수정하자 원본 vector인 myVec도 함께 변경되었다.
이는 span이 실제 데이터에 대한 직접적인 뷰라는 의미이다.
원본 데이터를 실수로라도 수정하지 않도록 하려면, const 요소 타입의 span을 생성하면 된다.
template<typename T>
void Print(std::span<const T> container)
{
for (int i = 0; i < container.size(); i++)
cout << container[i] << " ";
cout << endl;
}
int main()
{
std::span<const int> span1(myVec.data(), myVec.size());
std::span<const int> span2(span1.subspan(1, span1.size() - 3));
// 오류 발생
// std::transform(span1.begin(), span1.end(), span1.begin(), [](int i) { return i * i; });
Print<int>(span1);
Print<int>(myVec);
결과: 1 2 3 4 5
1 2 3 4 5
}
이는 실수로 데이터가 변경되는 것을 컴파일 타임에 막아주는 안전 장치가 된다.
이번에는 std::span에 대해 살펴보았다.
개인적으로 이 STL은 앞으로 실제 개발 과정에서 충분히 사용할 일이 있을 것 같다.
특히 C 스타일 배열과도 자연스럽게 호환되기 때문에,
기존 코드와의 통합이나 이전 프로젝트의 리팩토링에서도 부담 없이 활용할 수 있다는 점이 매력적이다.
이런 특성은 단순한 편의성을 넘어서 코드의 확장성과 유지보수성을 높이는 데에도 중요한 역할을 한다.
앞서 언급했듯이, 호환성 있는 인터페이스 설계는 장기적으로 코드의 유연성과 재사용성을 크게 높여준다.
C++20에는 이처럼 실용적이고 개발자의 작업을 덜어주는 기능들이 많이 포함되어 있다.
앞으로도 다양한 기능들을 하나씩 익혀가며, 더 깔끔하고 안정적인 코드를 작성할 수 있도록 계속해서 학습해 나가는 것이 좋겠다.
Inflearn [Rookiss][C++20 훑어보기]
전문가를 위한 C++(개정5판) P933~P935
C++공식문서