[C++20 새기능 소개] std::span

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을 사용하면 코드의 안전성, 유연성, 가독성을 모두 향상시킬 수 있습니다.

 

참고 자료:

 

 

반응형