[C++] std::span : 연속된 메모리를 복사 없이 넘기기

[C++] std::span : 연속된 메모리를 복사 없이 넘기기

안녕하세요. 이번 글에서는 C++20에서 추가된 std::span에 대해 알아보겠습니다.

배열이나 vector를 함수에 넘길 때 보통 포인터와 길이를 따로 전달하거나, vector를 const 참조로 받는 방식을 씁니다. 전자는 길이를 잘못 넘기기 쉽고, 후자는 C 배열이나 std::array에는 쓸 수 없습니다. span은 연속된 메모리 구간을 포인터 + 길이 한 쌍으로 묶어서 가리키는 가벼운 뷰(view)입니다. 데이터를 복사하지 않고, 소유하지도 않습니다.

기본 사용법

헤더는 <span>이며 C++20 이상(-std=c++20)이 필요합니다. C 배열, std::array, std::vector를 모두 같은 span 매개변수로 받을 수 있습니다.

#include <array>
#include <iostream>
#include <span>
#include <vector>
using namespace std;

int Sum(span<const int> data)
{
    int total = 0;
    for (int n : data)
        total += n;
    return total;
}

int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    array<int, 3> a = { 10, 20, 30 };
    vector<int> v = { 100, 200 };

    cout << Sum(arr) << "\n";   // 15
    cout << Sum(a) << "\n";     // 60
    cout << Sum(v) << "\n";     // 300
}

읽기만 하는 함수라면 span<const int>로 받고, 값을 수정해야 한다면 span<int>로 받습니다.

주요 멤버 함수

함수설명
size(), empty()요소 개수, 비어 있는지 여부
operator[], front(), back()요소 접근 (범위 검사는 하지 않음)
data()첫 요소를 가리키는 포인터
first(n), last(n)앞/뒤 n개를 가리키는 span
subspan(offset, count)offset부터 count개를 가리키는 span
begin(), end()범위 기반 for문, 알고리즘과 함께 사용

부분 구간 다루기

first, last, subspan은 복사 없이 같은 메모리의 일부를 가리키는 새 span을 돌려줍니다. 배열을 잘라서 처리하는 코드를 간단하게 쓸 수 있습니다.

#include <iostream>
#include <span>
#include <vector>
using namespace std;

void Print(span<const int> s)
{
    for (int n : s)
        cout << n << " ";
    cout << "\n";
}

int main()
{
    vector<int> v = { 1, 2, 3, 4, 5, 6 };
    span<int> all(v);

    Print(all.first(2));        // 1 2
    Print(all.last(2));         // 5 6
    Print(all.subspan(1, 3));   // 2 3 4

    // span으로 수정하면 원본 vector가 바뀜
    all.subspan(0, 2)[0] = 99;
    Print(v);                   // 99 2 3 4 5 6
}

고정 크기 span

span<int, 3>처럼 두 번째 템플릿 인자로 크기를 주면 길이가 컴파일 타임에 정해집니다. 지정하지 않으면 std::dynamic_extent로 간주되어 길이를 런타임에 가집니다. 크기가 맞지 않는 배열을 넘기면 컴파일 오류가 나므로 길이가 정해진 인터페이스에 적합합니다.

주의할 점

  • span은 소유하지 않으므로 가리키는 원본이 사라지면 dangling 상태가 됩니다. 지역 vector를 가리킨 span을 반환하면 안 됩니다.
  • vector에 요소를 추가하다가 재할당이 일어나면 기존 span은 무효가 됩니다.
  • operator[]는 범위를 검사하지 않습니다. 범위를 벗어나면 정의되지 않은 동작입니다.
  • 문자열을 읽기 전용으로 다룰 때는 span<const char>보다 std::string_view가 더 적합합니다.
  • 메모리가 연속되지 않은 컨테이너(list, deque 등)는 span으로 가리킬 수 없습니다.

참고

이상으로 std::span에 대해 간단히 알아보았습니다.