C++20의 span으로 배열과 컨테이너 접근을 안전하게 처리하기

C++20에서는 배열과 컨테이너에 대한 안전한 접근을 위해 std::span을 도입했습니다. 기존의 원시 포인터를 사용한 배열 접근 방식은 종종 버퍼 오버플로우, 범위 초과 접근 등의 메모리 안전 문제를 일으킬 수 있습니다.

std::span은 이러한 문제를 해결하고, 보다 명확하고 안정적인 코드 작성을 돕는 새로운 기능입니다. 이를 통해 개발자는 복사 없이 배열과 컨테이너 데이터를 참조할 수 있으며, 크기 정보를 포함한 안전한 인터페이스를 제공할 수 있습니다.

본 기사에서는 std::span의 개념과 사용법, 기존 배열 및 컨테이너와의 차이점, 그리고 실용적인 활용법을 설명하여 C++ 개발자가 보다 안전하고 효율적인 코드를 작성할 수 있도록 안내합니다.

목차
  1. std::span 개요와 도입 배경
    1. 기존 포인터 및 배열 사용의 문제점
    2. std::span의 도입 목적
  2. std::span의 기본 개념 및 사용법
    1. std::span의 기본적인 정의
    2. std::span의 주요 기능
    3. std::span을 활용한 안전한 함수 인터페이스
  3. std::span과 배열의 차이점
    1. 기존 배열과 std::span의 주요 차이점
    2. 배열을 사용한 기존 방식의 문제점
    3. std::span을 활용한 안전한 대안
    4. std::span과 배열 슬라이싱
    5. std::span을 사용할 때의 주의점
    6. 정리
  4. std::span을 활용한 함수 인터페이스 개선
    1. 기존 포인터 기반 함수 인터페이스의 문제점
    2. std::span을 활용한 개선된 함수 인터페이스
    3. std::span의 추가 기능: 부분 배열 전달
    4. std::span을 활용한 다차원 배열 지원
    5. 정리
  5. std::span과 std::vector, std::array 비교
    1. std::span과 std::vector, std::array의 주요 차이점
    2. std::span과 std::vector 비교
    3. std::span과 std::array 비교
    4. std::span이 적합한 경우
    5. std::vector, std::array가 적합한 경우
    6. 정리
  6. std::span을 활용한 메모리 안전성 강화
    1. 원시 포인터 기반 접근의 문제점
    2. std::span을 사용한 안전한 대안
    3. 경계 초과 접근 방지
    4. use-after-free 방지
    5. std::span과 const 참조를 활용한 불변 데이터 보호
    6. 정리
  7. std::span과 다차원 배열 처리
    1. 기존 방식의 다차원 배열 처리 문제
    2. std::span을 활용한 개선된 방식
    3. 개선점:
    4. std::span을 사용한 2D 배열 뷰
    5. 설명:
    6. 다차원 배열을 std::span으로 변환
    7. 핵심 포인트:
    8. std::mdspan을 활용한 다차원 배열 지원 (C++23)
    9. 정리
  8. std::span 활용 예제 및 성능 고려 사항
    1. std::span을 활용한 안전한 데이터 처리 예제
    2. 핵심 포인트:
    3. 부분 배열 처리 및 슬라이싱 최적화
    4. 핵심 포인트:
    5. std::span 성능 최적화 고려 사항
    6. std::span을 활용한 성능 최적화 예제
    7. 핵심 포인트:
    8. 정리
  9. 요약

std::span 개요와 도입 배경

C++20에서 도입된 std::span은 배열이나 컨테이너 데이터를 안전하게 참조할 수 있도록 설계된 경량 뷰(view) 클래스입니다. 기존의 배열과 포인터 기반 접근 방식은 여러 가지 문제점을 내포하고 있어 유지보수성과 안정성을 저하시킬 수 있습니다.

기존 포인터 및 배열 사용의 문제점

C++에서 배열을 다룰 때 보편적으로 사용하는 방법은 원시 포인터를 통해 데이터를 전달하는 것입니다. 그러나 이 방법에는 여러 가지 단점이 존재합니다.

  1. 크기 정보 손실
  • 배열을 함수에 전달할 때 포인터로 변환되면 배열의 크기 정보가 사라지므로, 추가적인 크기 매개변수를 사용해야 합니다.
   void process(int* data, size_t size) {
       for (size_t i = 0; i < size; ++i) {
           std::cout << data[i] << std::endl;
       }
   }
  1. 경계를 초과하는 접근 위험
  • 유효한 크기 정보를 보장할 수 없기 때문에, 실수로 배열 범위를 초과하는 접근이 발생할 가능성이 큽니다.
   int arr[5] = {1, 2, 3, 4, 5};
   process(arr, 10); // 잘못된 크기 전달로 인해 경계 초과 발생 가능
  1. 메모리 안전성 문제
  • 포인터를 사용하면 동적 할당된 메모리를 수동으로 관리해야 하며, 메모리 누수 및 접근 오류가 발생할 위험이 큽니다.

std::span의 도입 목적

std::span은 위와 같은 문제를 해결하기 위해 등장한 C++20의 기능으로, 다음과 같은 장점을 제공합니다.

  • 크기 정보를 포함하는 경량 뷰 클래스
  • 배열 크기를 직접 포함하여 크기 정보 손실 문제를 방지합니다.
  • 복사 비용 없는 참조 기능
  • 데이터를 복사하지 않고 참조만 하므로 성능 저하 없이 안전한 인터페이스 제공이 가능합니다.
  • 컨테이너와 원시 배열 모두 지원
  • std::vector, std::array, 원시 배열 등 다양한 컨테이너를 처리할 수 있습니다.

C++20에서 std::span이 추가됨으로써 배열 및 컨테이너를 더욱 안전하고 편리하게 다룰 수 있게 되었습니다. 다음 장에서는 std::span의 기본 개념과 사용법을 자세히 살펴보겠습니다.

std::span의 기본 개념 및 사용법

C++20에서 새롭게 도입된 std::span은 배열, std::vector, std::array 등의 컨테이너 데이터를 복사 없이 참조할 수 있는 경량 뷰(view)입니다. 이를 사용하면 크기 정보를 유지하면서 안전하게 배열과 컨테이너에 접근할 수 있습니다.

std::span의 기본적인 정의

std::span<span> 헤더 파일을 포함하여 사용할 수 있으며, 배열 및 컨테이너를 포인터처럼 다룰 수 있습니다. 하지만 포인터와 달리 크기 정보를 유지하므로 보다 안전한 접근이 가능합니다.

#include <iostream>
#include <span>
#include <vector>

void print(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {6, 7, 8, 9, 10};

    print(arr);  // 배열을 span에 전달
    print(vec);  // vector를 span에 전달

    return 0;
}

위 코드에서 print() 함수는 std::span<int>를 매개변수로 사용하므로, 배열과 std::vector를 같은 방식으로 처리할 수 있습니다. 이 과정에서 데이터를 복사하지 않고 참조만 하기 때문에 성능상 이점이 있습니다.

std::span의 주요 기능

std::span을 사용하면 크기 정보를 유지하면서 여러 유용한 기능을 사용할 수 있습니다.

  1. 배열과 컨테이너 참조
  • std::span은 원시 배열, std::vector, std::array를 직접 참조할 수 있습니다.
  1. 부분 범위(span slicing) 지원
  • std::span은 부분 범위를 손쉽게 추출할 수 있습니다.
   int arr[] = {10, 20, 30, 40, 50};
   std::span<int> sp(arr);

   std::span<int> sub = sp.subspan(1, 3); // 20, 30, 40
  1. 정적 크기와 동적 크기 지원
  • std::span<int>는 기본적으로 크기를 동적으로 관리하지만, std::span<int, N>과 같이 정적 크기를 지정할 수도 있습니다.
   std::span<int, 5> fixed_size_span(arr); // 정적 크기 지정

std::span을 활용한 안전한 함수 인터페이스

기존에는 포인터를 사용하여 배열을 함수에 전달했지만, 크기 정보를 보장할 수 없었습니다. std::span을 사용하면 크기 정보가 유지되므로 보다 안전한 함수 인터페이스를 만들 수 있습니다.

void process(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    process(arr);  // 크기 정보 유지
}

위처럼 std::span을 사용하면 별도의 크기 인자를 전달할 필요가 없으며, 컨테이너 종류에 관계없이 동일한 방식으로 데이터를 처리할 수 있습니다.

다음 장에서는 std::span과 기존 배열의 차이점을 비교하며 보다 깊이 있는 내용을 다루겠습니다.

std::span과 배열의 차이점

C++에서 배열을 다룰 때 전통적으로 원시 배열(int arr[]) 또는 std::vector, std::array와 같은 컨테이너를 사용해 왔습니다. 하지만 기존 배열 방식은 여러 문제점을 내포하고 있으며, C++20에서 새롭게 도입된 std::span을 사용하면 이러한 문제를 효과적으로 해결할 수 있습니다.

기존 배열과 std::span의 주요 차이점

비교 항목원시 배열 (int arr[])std::span<int>
크기 정보 유지X (포인터로 변환 시 손실)O (크기 정보 포함)
컨테이너 지원X (std::vector, std::array 직접 전달 불가)O (std::vector, std::array도 참조 가능)
복사 비용없음 (참조 전달 가능)없음 (경량 뷰로 복사 비용 없음)
배열 슬라이싱 지원X (수동으로 처리 필요)O (subspan() 사용 가능)
안전성낮음 (경계 초과 가능)높음 (경계 초과 위험 감소)

배열을 사용한 기존 방식의 문제점

기존의 원시 배열은 크기 정보를 유지하지 않으며, 포인터를 사용하여 데이터를 전달할 때 경계 초과 접근이 발생할 위험이 큽니다.

void print(int* arr, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    print(arr, 5); // 크기 정보를 별도로 전달해야 함
}

위 코드에서는 배열을 int*로 변환하여 전달하므로, 크기 정보를 별도의 인자로 함께 전달해야 합니다. 실수로 잘못된 크기를 전달하면 경계 초과 접근이 발생할 수 있습니다.

std::span을 활용한 안전한 대안

std::span을 사용하면 크기 정보가 포함되므로 별도의 크기 인자를 전달할 필요가 없습니다.

#include <iostream>
#include <span>

void print(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    print(arr); // 크기 정보 자동 유지
}

이 코드에서는 std::span을 사용하여 arr을 참조하므로, 배열의 크기 정보를 유지한 채 전달할 수 있습니다. 이로 인해 보다 안전한 데이터 처리가 가능합니다.

std::span과 배열 슬라이싱

std::span은 원본 데이터를 복사하지 않고 부분 배열을 쉽게 추출할 수 있는 기능을 제공합니다.

#include <iostream>
#include <span>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    std::span<int> sp(arr);

    std::span<int> sub = sp.subspan(1, 3); // 20, 30, 40 추출
    for (int v : sub) {
        std::cout << v << " ";
    }

    return 0;
}

위 코드에서 subspan(1, 3)을 호출하면 원본 배열에서 1번 인덱스부터 3개의 요소를 포함하는 새로운 std::span 객체가 생성됩니다. 이 과정에서 데이터 복사가 발생하지 않으므로 성능상 이점이 있습니다.

std::span을 사용할 때의 주의점

  • std::span은 원본 데이터의 수명을 관리하지 않습니다. 따라서 std::span이 참조하는 배열이 삭제되면 std::span도 유효하지 않게 됩니다.
  • std::spanstd::vector.data()를 사용해 생성하는 경우, std::vector의 크기가 변경되면 std::span이 가리키는 데이터가 무효화될 수 있습니다.

정리

std::span은 배열 및 컨테이너를 보다 안전하게 참조할 수 있도록 도와주는 C++20의 기능입니다. 기존의 원시 배열이 갖는 크기 정보 손실 문제와 포인터 기반 접근의 위험성을 해결하며, 함수 인터페이스를 더욱 단순하고 직관적으로 만들 수 있습니다.

다음 장에서는 std::span을 활용하여 함수 인터페이스를 개선하는 방법을 살펴보겠습니다.

std::span을 활용한 함수 인터페이스 개선

C++에서 배열이나 컨테이너를 함수에 전달할 때 전통적으로 포인터를 사용해 왔습니다. 하지만 이 방식은 크기 정보 손실, 범위 초과 접근, 컨테이너 유형별 별도 오버로드 필요 등의 문제를 발생시킬 수 있습니다.

C++20의 std::span을 활용하면 이러한 문제를 해결하여 더 간결하고 안전한 함수 인터페이스를 설계할 수 있습니다.

기존 포인터 기반 함수 인터페이스의 문제점

기존의 포인터 기반 함수는 배열의 크기 정보를 유지하지 않으므로, 별도의 크기 매개변수를 전달해야 합니다.

#include <iostream>

// 크기 정보를 별도로 전달해야 함
void process(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        std::cout << data[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    process(arr, 5);  // 크기 전달 필수
}

이 방식에는 몇 가지 문제가 있습니다.

  1. 잘못된 크기 전달 가능
  • process(arr, 10);과 같이 잘못된 크기를 전달하면 정의되지 않은 동작(UB)이 발생할 수 있습니다.
  1. 컨테이너 타입별로 별도 함수 필요
  • std::vector, std::array 등을 지원하려면 각 컨테이너 타입에 대해 별도의 함수 오버로드가 필요합니다.

std::span을 활용한 개선된 함수 인터페이스

std::span을 사용하면 크기 정보를 포함하면서도 여러 컨테이너 타입을 한 번에 처리할 수 있습니다.

#include <iostream>
#include <span>
#include <vector>

void process(std::span<int> data) { // 크기 정보 포함
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {6, 7, 8, 9, 10};

    process(arr);  // 배열 지원
    process(vec);  // std::vector도 지원

    return 0;
}

위 코드에서 process(std::span<int> data) 함수는

  • 배열 (int arr[])
  • std::vector
  • std::array

을 동일한 방식으로 처리할 수 있습니다. 추가적인 크기 매개변수가 필요 없으며, 컨테이너 유형에 관계없이 일관된 인터페이스를 제공합니다.

std::span의 추가 기능: 부분 배열 전달

std::span을 사용하면 함수 인터페이스가 더욱 유연해집니다. 예를 들어, 배열의 일부만 전달할 수도 있습니다.

int arr[] = {10, 20, 30, 40, 50};
std::span<int> sp(arr);

process(sp.subspan(1, 3)); // 20, 30, 40만 전달

이는 기존 포인터 기반 접근 방식에서는 불가능한 깔끔한 구현 방법입니다.

std::span을 활용한 다차원 배열 지원

기존의 포인터 방식은 다차원 배열을 함수에 전달할 때 복잡한 문법을 요구했지만, std::span을 사용하면 보다 직관적인 인터페이스를 제공할 수 있습니다.

#include <iostream>
#include <span>

void print_matrix(std::span<int, 3> row) { // 행 단위 처리
    for (int value : row) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int matrix[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };

    for (auto& row : matrix) {
        print_matrix(row); // 개별 행을 std::span으로 전달
    }
}

이와 같이 std::span을 사용하면 크기 정보를 유지하면서 다차원 배열을 손쉽게 다룰 수 있습니다.

정리

std::span을 활용하면 기존의 포인터 기반 함수 인터페이스에서 발생하는 크기 정보 손실, 잘못된 크기 전달, 컨테이너 타입별 오버로드 필요성 등의 문제를 해결할 수 있습니다.

  • 자동 크기 유지: 별도의 크기 매개변수가 필요 없음
  • 배열, std::vector, std::array를 동일한 방식으로 처리 가능
  • 부분 배열 슬라이싱(subspan) 지원
  • 다차원 배열을 보다 직관적으로 다룰 수 있음

다음 장에서는 std::spanstd::vector, std::array의 차이점을 비교하여 각각의 장단점을 살펴보겠습니다.

std::span과 std::vector, std::array 비교

C++에서는 배열과 컨테이너를 다루기 위해 여러 가지 방법이 존재합니다. 그중 대표적인 것이 std::vector, std::array, 그리고 C++20에서 새롭게 도입된 std::span입니다. 이들은 모두 데이터를 저장하고 참조하는 기능을 제공하지만, 내부적인 동작 방식과 사용 목적에서 차이가 있습니다.

이번 장에서는 std::spanstd::vector, std::array의 차이점을 비교하며, 각 방식의 장단점을 살펴보겠습니다.

std::span과 std::vector, std::array의 주요 차이점

특징std::spanstd::vectorstd::array
데이터 소유 여부X (참조)O (소유)O (소유)
메모리 할당없음동적 할당정적 할당
크기 조정 가능 여부XO (resize() 가능)X (컴파일 시 크기 고정)
배열과 컨테이너 지원O (int arr[], std::vector, std::array 모두 참조 가능)X (자체 데이터만 저장)X (자체 데이터만 저장)
복사 비용없음 (참조)있음 (복사 시 비용 발생)있음 (복사 시 비용 발생)
부분 참조 가능 여부O (subspan() 지원)XX

std::span과 std::vector 비교

std::vector는 크기가 동적으로 변하는 배열로, 동적 메모리 할당을 사용하여 데이터를 저장합니다. 반면, std::span은 메모리를 소유하지 않으며, 이미 존재하는 데이터를 참조하는 역할을 합니다.

#include <iostream>
#include <span>
#include <vector>

void print(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    print(vec);  // std::vector를 std::span으로 전달

    return 0;
}

위 코드에서 print() 함수는 std::span<int>을 매개변수로 받아 std::vector<int>를 동일한 방식으로 처리할 수 있습니다. 하지만 std::span은 데이터를 소유하지 않으므로, vec의 크기를 변경하는 작업(resize(), push_back())을 하면 std::span이 참조하는 데이터가 무효화될 수 있습니다.

std::span과 std::array 비교

std::array는 정적 크기를 갖는 배열 컨테이너로, std::vector와 달리 크기가 변경되지 않습니다. std::spanstd::array의 데이터를 참조하는 용도로 사용될 수 있습니다.

#include <iostream>
#include <span>
#include <array>

void print(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::array<int, 5> arr = {10, 20, 30, 40, 50};

    print(arr);  // std::array도 std::span으로 전달 가능

    return 0;
}

std::span을 사용하면 std::array의 크기를 유지한 채 참조할 수 있으며, 데이터를 복사하지 않고도 안전하게 처리할 수 있습니다.

std::span이 적합한 경우

다음과 같은 경우 std::span을 사용하는 것이 적절합니다.

  1. 배열과 컨테이너를 동일한 방식으로 다루고 싶을 때
  • 함수에서 std::vector, std::array, 원시 배열을 한 번에 처리하고 싶다면 std::span이 유용합니다.
  1. 데이터 복사를 피해야 할 때
  • 데이터를 소유하지 않고 참조만 하므로, 복사 비용을 줄일 수 있습니다.
  1. 부분 배열 참조가 필요할 때
  • subspan()을 사용하면 특정 범위의 데이터를 별도로 참조할 수 있습니다.
int arr[] = {10, 20, 30, 40, 50};
std::span<int> sp(arr);

std::span<int> sub = sp.subspan(1, 3); // 20, 30, 40만 참조

std::vector, std::array가 적합한 경우

반면, std::span이 적절하지 않은 경우도 있습니다.

  • 데이터를 소유해야 할 경우
  • std::span은 참조만 가능하므로, 데이터를 직접 저장해야 하는 경우 std::vectorstd::array를 사용해야 합니다.
  • 크기 변경이 필요한 경우
  • std::span은 크기를 조정할 수 없기 때문에, 동적으로 크기를 변경해야 한다면 std::vector가 적합합니다.

정리

사용 상황적합한 컨테이너
크기 변경 필요std::vector
데이터 복사가 필요 없는 참조std::span
정적 크기의 배열 필요std::array
원시 배열을 함수에 안전하게 전달std::span

std::span은 원시 배열, std::vector, std::array를 모두 동일한 방식으로 다룰 수 있도록 하며, 크기 정보를 유지하면서도 데이터를 복사하지 않고 참조할 수 있다는 점에서 매우 유용합니다. 그러나 데이터 소유권이 필요하거나 크기 변경이 필요한 경우에는 std::vector 또는 std::array를 사용하는 것이 더 적절합니다.

다음 장에서는 std::span을 활용하여 메모리 안전성을 강화하는 방법을 살펴보겠습니다.

std::span을 활용한 메모리 안전성 강화

C++에서는 원시 포인터(int*)를 사용하여 배열을 다룰 때, 경계를 초과하는 접근(buffer overflow)이나 잘못된 메모리 접근(use-after-free)과 같은 위험이 발생할 수 있습니다. std::span을 활용하면 이러한 문제를 효과적으로 방지하여 보다 안전한 코드를 작성할 수 있습니다.

원시 포인터 기반 접근의 문제점

기존의 C++ 코드에서는 원시 포인터를 사용하여 배열을 전달하는 경우가 많았습니다. 그러나 이 방식은 크기 정보를 보장하지 않으며, 경계를 초과하는 잘못된 접근이 발생할 가능성이 큽니다.

#include <iostream>

void print(int* data, size_t size) {
    for (size_t i = 0; i <= size; ++i) { // 크기 초과 접근 발생 가능
        std::cout << data[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    print(arr, 5); // 정상적인 호출이지만 내부적으로 오류 발생 가능
}

위 코드에서 size보다 1만큼 더 큰 범위까지 접근하는 실수가 발생하더라도 컴파일러는 이를 감지할 수 없습니다. 이러한 실수는 프로그램 충돌이나 보안 취약점으로 이어질 수 있습니다.

std::span을 사용한 안전한 대안

std::span을 사용하면 배열을 참조하면서도 크기 정보를 유지할 수 있으며, 경계를 초과하는 접근을 방지할 수 있습니다.

#include <iostream>
#include <span>

void print(std::span<int> data) {
    for (int value : data) { // 범위를 자동으로 유지
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    print(arr); // 크기 정보 자동 유지
}

위 코드에서는 std::span<int>을 사용하여 배열의 크기를 유지하면서도 안전하게 데이터를 반복(iteration)할 수 있습니다.

경계 초과 접근 방지

std::span은 내부적으로 크기 정보를 포함하고 있으므로, 범위를 벗어난 잘못된 접근을 방지할 수 있습니다. 예를 들어, subspan()을 활용하면 특정 범위의 데이터만 안전하게 다룰 수 있습니다.

#include <iostream>
#include <span>

void process(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    std::span<int> sp(arr);

    process(sp.subspan(1, 3)); // 안전하게 20, 30, 40만 전달

    return 0;
}

이 방식은 원본 배열의 데이터를 참조하면서도 특정 범위의 요소만 안전하게 사용할 수 있도록 합니다.

use-after-free 방지

std::span은 데이터의 소유권을 갖지 않기 때문에, 동적 할당된 데이터를 다룰 때는 주의가 필요합니다. 예를 들어, 다음 코드처럼 std::vector.data()를 사용하여 std::span을 생성하면, 벡터가 삭제된 후 std::span이 유효하지 않게 됩니다.

std::span<int> getSpan() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    return std::span<int>(vec.data(), vec.size()); // 위험한 코드
} // vec가 소멸되면 span도 무효화됨

위 코드는 std::span이 참조하는 데이터가 함수 종료 시 벡터와 함께 소멸되므로, 이후 std::span을 사용하면 정의되지 않은 동작(UB)이 발생할 수 있습니다.

이 문제를 방지하려면 std::span이 참조하는 데이터가 함수가 반환된 이후에도 유효한지 반드시 확인해야 합니다.

std::span과 const 참조를 활용한 불변 데이터 보호

std::spanconst 키워드와 함께 사용하여 불변(immutable) 데이터를 안전하게 참조할 수도 있습니다.

void print_readonly(std::span<const int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    const int arr[] = {100, 200, 300};
    print_readonly(arr); // 불변 데이터 전달 가능
}

위 코드에서는 std::span<const int>을 사용하여 데이터를 읽기 전용으로 참조할 수 있으며, 함수 내부에서 데이터를 수정하는 실수를 방지할 수 있습니다.

정리

  • std::span배열 및 컨테이너의 크기 정보를 유지하면서 안전하게 참조할 수 있도록 돕는다.
  • 경계 초과 접근(buffer overflow)을 방지하며, subspan()을 활용해 안전한 부분 참조가 가능하다.
  • use-after-free 문제를 방지하려면 std::span이 참조하는 데이터의 유효성을 반드시 유지해야 한다.
  • const와 함께 사용하여 읽기 전용 데이터를 보호할 수 있다.

다음 장에서는 std::span을 활용한 다차원 배열 처리 기법을 살펴보겠습니다.

std::span과 다차원 배열 처리

C++에서는 다차원 배열을 사용할 때 메모리 관리가 복잡해질 수 있으며, 특히 함수에 전달할 때 불편한 점이 많습니다. 기존 방식에서는 배열의 크기 정보를 유지하기 어렵고, 포인터 연산이 필요했습니다.

C++20의 std::span을 활용하면 다차원 배열을 보다 안전하고 직관적으로 다룰 수 있습니다. 이번 장에서는 std::span을 사용하여 다차원 배열을 처리하는 방법을 살펴보겠습니다.

기존 방식의 다차원 배열 처리 문제

다차원 배열을 함수에 전달하는 기존 방식은 가독성이 떨어지고, 유연성이 부족합니다.

#include <iostream>

void print_matrix(int matrix[][3], size_t rows) { // 크기 정보를 별도로 전달해야 함
    for (size_t i = 0; i < rows; ++i) {
        for (size_t j = 0; j < 3; ++j) {
            std::cout << matrix[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print_matrix(matrix, 2);
}

위 코드에서는 행(row) 크기 정보를 별도로 전달해야 하며, 열(column) 크기는 고정되어 있어 유연성이 부족합니다.

std::span을 활용한 개선된 방식

std::span을 사용하면 다차원 배열을 보다 직관적으로 참조할 수 있습니다.

#include <iostream>
#include <span>

void print_matrix(std::span<std::span<int>> matrix) {
    for (auto row : matrix) {
        for (int value : row) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int row1[] = {1, 2, 3};
    int row2[] = {4, 5, 6};

    std::span<int> rows[] = {row1, row2};
    print_matrix(rows); // 다차원 배열을 span으로 처리

    return 0;
}

개선점:

  • 유연한 크기 처리: std::span을 중첩하여 행과 열 크기를 유연하게 설정할 수 있음.
  • 별도의 크기 정보 전달 불필요: 크기 정보를 자동으로 유지하여 함수 인터페이스가 간결해짐.

std::span을 사용한 2D 배열 뷰

C++에서는 2D 배열을 연속된 메모리로 할당하는 경우가 많으며, std::span을 사용하면 이를 효과적으로 참조할 수 있습니다.

#include <iostream>
#include <span>

void print_matrix(std::span<int, 6> matrix, size_t cols) {
    for (size_t i = 0; i < matrix.size() / cols; ++i) {
        for (size_t j = 0; j < cols; ++j) {
            std::cout << matrix[i * cols + j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print_matrix(std::span<int>(matrix[0], 6), 3); // 2×3 배열을 1D span으로 변환

    return 0;
}

설명:

  • std::span<int, 6>을 사용하여 전체 배열을 1D 뷰로 참조.
  • cols 값을 사용하여 2D 형태로 출력 가능.
  • 별도의 포인터 연산 없이 안전하게 다차원 배열을 처리할 수 있음.

다차원 배열을 std::span으로 변환

일반적으로 std::span은 1차원 배열을 참조하는 용도로 사용되지만, 다차원 배열을 다룰 때는 1D 형태로 변환하여 활용할 수 있습니다.

#include <iostream>
#include <span>

void process_2d(std::span<int> data, size_t rows, size_t cols) {
    for (size_t i = 0; i < rows; ++i) {
        for (size_t j = 0; j < cols; ++j) {
            std::cout << data[i * cols + j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    process_2d(std::span<int>(matrix[0], 12), 3, 4); // 3×4 배열을 span으로 변환

    return 0;
}

핵심 포인트:

  • std::span<int>을 사용하여 1D 뷰로 변환 후, rowscols 정보를 이용하여 2D처럼 처리.
  • 다차원 배열을 일반적인 C++ 스타일과 호환되게 다룰 수 있음.
  • 포인터를 직접 사용하지 않으므로 보다 안전한 코드 작성 가능.

std::mdspan을 활용한 다차원 배열 지원 (C++23)

C++23에서는 std::mdspan이 추가되어 다차원 배열을 보다 직관적으로 참조할 수 있도록 지원합니다.

#include <iostream>
#include <mdspan>

int main() {
    int matrix[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    std::mdspan m(matrix);

    for (size_t i = 0; i < m.extent(0); ++i) {
        for (size_t j = 0; j < m.extent(1); ++j) {
            std::cout << m(i, j) << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

C++23의 std::mdspan을 활용하면 다차원 배열을 보다 효율적으로 관리할 수 있으며, std::span보다 더 직관적인 인터페이스를 제공하게 됩니다.

정리

  • std::span을 사용하면 다차원 배열을 보다 유연하게 처리할 수 있다.
  • 기존 포인터 방식보다 가독성이 좋고 안전한 코드 작성이 가능하다.
  • std::span<int>을 사용하면 1D 형태로 변환하여 2D 데이터를 다룰 수 있다.
  • C++23의 std::mdspan은 다차원 배열 처리를 더욱 효율적으로 지원한다.

다음 장에서는 std::span의 실제 활용 예제 및 성능 최적화를 살펴보겠습니다.

std::span 활용 예제 및 성능 고려 사항

std::span은 배열과 컨테이너를 효율적으로 참조할 수 있는 기능을 제공하지만, 잘못 사용하면 성능 저하나 메모리 안전성 문제를 일으킬 수도 있습니다. 이번 장에서는 std::span을 활용한 실제 코드 예제와 성능 최적화에 대한 고려 사항을 살펴보겠습니다.


std::span을 활용한 안전한 데이터 처리 예제

기존 C++에서는 배열을 함수에 전달할 때 원시 포인터(int*)를 사용했지만, 이는 크기 정보 손실과 경계 초과 접근의 위험이 있었습니다. std::span을 활용하면 이러한 문제를 방지하면서도 데이터를 효율적으로 다룰 수 있습니다.

#include <iostream>
#include <span>

void process_data(std::span<int> data) {
    for (int& value : data) {
        value *= 2;  // 데이터 변환
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};

    process_data(arr); // 원본 데이터 참조

    for (int value : arr) {
        std::cout << value << " "; // 2 4 6 8 10
    }
    return 0;
}

핵심 포인트:

  • std::span을 사용하면 데이터를 복사하지 않고 참조하여 직접 수정할 수 있음.
  • 원본 배열(arr)이 함수에서 변경되므로 복사 비용이 없음.
  • 기존 포인터 기반 접근보다 더 안전하고 직관적인 코드 작성 가능.

부분 배열 처리 및 슬라이싱 최적화

std::span을 활용하면 데이터의 특정 부분만 안전하게 참조할 수 있습니다.

#include <iostream>
#include <span>

void process_partial(std::span<int> data) {
    for (int value : data) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
}

int main() {
    int arr[] = {10, 20, 30, 40, 50};

    std::span<int> full_span(arr);
    std::span<int> sub_span = full_span.subspan(1, 3); // 20, 30, 40만 참조

    process_partial(sub_span); // 부분 데이터만 출력
}

핵심 포인트:

  • subspan(start, count)을 사용하여 부분 데이터를 안전하게 참조할 수 있음.
  • 원본 배열을 변경하지 않고 필요한 부분만 추출하여 처리 가능.
  • 추가적인 메모리 할당 없이 즉시 데이터 접근 가능, 즉 성능 저하 없이 활용 가능.

std::span 성능 최적화 고려 사항

1. 불필요한 복사 방지

std::span은 데이터를 참조할 뿐, 소유권을 가지지 않습니다. 따라서 복사 비용을 줄이려면 std::vector 또는 std::array의 참조를 직접 사용하는 것이 좋습니다.

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp(vec);  // 참조만 유지 (복사 없음)

2. 유효한 데이터 참조 보장

std::span은 포인터와 유사하게 동작하므로, 참조하는 데이터가 유효한지 항상 주의해야 합니다.

std::span<int> create_span() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    return std::span<int>(vec);  // 잘못된 코드! vec는 함수 종료 시 소멸됨
}

위 코드에서 std::span<int>vec을 참조하지만, vec이 소멸되므로 유효하지 않은 데이터에 접근할 위험이 있음.

해결 방법:

  • std::vector<int> 자체를 반환하거나
  • std::span을 참조할 때 함수 외부에서 생성된 데이터를 사용해야 함.

std::span을 활용한 성능 최적화 예제

데이터를 여러 스레드에서 병렬로 처리할 때 std::span을 활용하면 복사 비용을 최소화할 수 있습니다.

#include <iostream>
#include <span>
#include <vector>
#include <thread>

void process_chunk(std::span<int> chunk) {
    for (int& value : chunk) {
        value *= 2; // 데이터를 2배로 변환
    }
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8};

    std::span<int> first_half(data.begin(), data.begin() + data.size() / 2);
    std::span<int> second_half(data.begin() + data.size() / 2, data.end());

    std::thread t1(process_chunk, first_half);
    std::thread t2(process_chunk, second_half);

    t1.join();
    t2.join();

    for (int value : data) {
        std::cout << value << " "; // 2 4 6 8 10 12 14 16
    }

    return 0;
}

핵심 포인트:

  • std::span을 사용하여 배열의 특정 부분을 안전하게 참조.
  • 복사 없이 여러 개의 스레드에서 데이터를 병렬로 처리 가능.
  • std::vector 또는 std::array를 참조하여 데이터 무효화를 방지.

정리

  • std::span을 활용하면 데이터를 복사하지 않고 참조할 수 있어 성능 최적화에 유리하다.
  • subspan()을 사용하여 부분 데이터를 효율적으로 참조 가능하다.
  • 다중 스레드 환경에서 안전하게 사용할 수 있으며, 불필요한 복사를 방지하여 성능을 향상할 수 있다.
  • std::span이 참조하는 데이터가 유효한지 반드시 확인해야 한다.

다음 장에서는 std::span의 전체적인 요약을 정리하며 활용할 수 있는 최종 가이드를 제공합니다.

요약

C++20에서 도입된 std::span은 배열과 컨테이너를 보다 안전하고 효율적으로 참조할 수 있도록 설계된 경량 뷰(view)입니다. 기존의 원시 포인터 기반 접근 방식이 가지는 크기 정보 손실, 경계 초과 접근, 불필요한 데이터 복사 등의 문제를 해결하는 데 큰 도움이 됩니다.

이번 기사에서 다룬 주요 내용은 다음과 같습니다.

  • std::span배열, std::vector, std::array 등 다양한 컨테이너를 참조 가능하며, 크기 정보를 유지하여 보다 안전한 데이터 처리를 지원합니다.
  • 기존 원시 포인터 기반 접근 방식과 달리, 경계를 초과하는 접근(buffer overflow)을 방지할 수 있습니다.
  • subspan()을 활용하면 부분 데이터만 안전하게 참조할 수 있으며, 복사 비용 없이 슬라이싱이 가능합니다.
  • 다차원 배열을 쉽게 다룰 수 있으며, std::span<int>을 사용하여 1D 뷰로 변환하는 방식도 유용합니다.
  • 병렬 연산과 다중 스레드 환경에서도 안전하게 활용 가능하며, 데이터 복사 없이 참조만을 활용하여 성능을 최적화할 수 있습니다.
  • 단, std::span은 데이터의 소유권을 가지지 않기 때문에, 참조 대상이 유효한지 항상 확인해야 합니다.

C++ 개발자라면 std::span을 적극 활용하여 보다 안전하고 성능 최적화된 코드를 작성할 수 있습니다. C++20을 도입한 프로젝트에서는 std::span을 표준 인터페이스로 활용하여 유지보수성을 높이고, 성능 개선 효과를 얻을 수 있을 것입니다.

목차
  1. std::span 개요와 도입 배경
    1. 기존 포인터 및 배열 사용의 문제점
    2. std::span의 도입 목적
  2. std::span의 기본 개념 및 사용법
    1. std::span의 기본적인 정의
    2. std::span의 주요 기능
    3. std::span을 활용한 안전한 함수 인터페이스
  3. std::span과 배열의 차이점
    1. 기존 배열과 std::span의 주요 차이점
    2. 배열을 사용한 기존 방식의 문제점
    3. std::span을 활용한 안전한 대안
    4. std::span과 배열 슬라이싱
    5. std::span을 사용할 때의 주의점
    6. 정리
  4. std::span을 활용한 함수 인터페이스 개선
    1. 기존 포인터 기반 함수 인터페이스의 문제점
    2. std::span을 활용한 개선된 함수 인터페이스
    3. std::span의 추가 기능: 부분 배열 전달
    4. std::span을 활용한 다차원 배열 지원
    5. 정리
  5. std::span과 std::vector, std::array 비교
    1. std::span과 std::vector, std::array의 주요 차이점
    2. std::span과 std::vector 비교
    3. std::span과 std::array 비교
    4. std::span이 적합한 경우
    5. std::vector, std::array가 적합한 경우
    6. 정리
  6. std::span을 활용한 메모리 안전성 강화
    1. 원시 포인터 기반 접근의 문제점
    2. std::span을 사용한 안전한 대안
    3. 경계 초과 접근 방지
    4. use-after-free 방지
    5. std::span과 const 참조를 활용한 불변 데이터 보호
    6. 정리
  7. std::span과 다차원 배열 처리
    1. 기존 방식의 다차원 배열 처리 문제
    2. std::span을 활용한 개선된 방식
    3. 개선점:
    4. std::span을 사용한 2D 배열 뷰
    5. 설명:
    6. 다차원 배열을 std::span으로 변환
    7. 핵심 포인트:
    8. std::mdspan을 활용한 다차원 배열 지원 (C++23)
    9. 정리
  8. std::span 활용 예제 및 성능 고려 사항
    1. std::span을 활용한 안전한 데이터 처리 예제
    2. 핵심 포인트:
    3. 부분 배열 처리 및 슬라이싱 최적화
    4. 핵심 포인트:
    5. std::span 성능 최적화 고려 사항
    6. std::span을 활용한 성능 최적화 예제
    7. 핵심 포인트:
    8. 정리
  9. 요약