C++20에서는 배열과 컨테이너에 대한 안전한 접근을 위해 std::span
을 도입했습니다. 기존의 원시 포인터를 사용한 배열 접근 방식은 종종 버퍼 오버플로우, 범위 초과 접근 등의 메모리 안전 문제를 일으킬 수 있습니다.
std::span
은 이러한 문제를 해결하고, 보다 명확하고 안정적인 코드 작성을 돕는 새로운 기능입니다. 이를 통해 개발자는 복사 없이 배열과 컨테이너 데이터를 참조할 수 있으며, 크기 정보를 포함한 안전한 인터페이스를 제공할 수 있습니다.
본 기사에서는 std::span
의 개념과 사용법, 기존 배열 및 컨테이너와의 차이점, 그리고 실용적인 활용법을 설명하여 C++ 개발자가 보다 안전하고 효율적인 코드를 작성할 수 있도록 안내합니다.
std::span 개요와 도입 배경
C++20에서 도입된 std::span
은 배열이나 컨테이너 데이터를 안전하게 참조할 수 있도록 설계된 경량 뷰(view) 클래스입니다. 기존의 배열과 포인터 기반 접근 방식은 여러 가지 문제점을 내포하고 있어 유지보수성과 안정성을 저하시킬 수 있습니다.
기존 포인터 및 배열 사용의 문제점
C++에서 배열을 다룰 때 보편적으로 사용하는 방법은 원시 포인터를 통해 데이터를 전달하는 것입니다. 그러나 이 방법에는 여러 가지 단점이 존재합니다.
- 크기 정보 손실
- 배열을 함수에 전달할 때 포인터로 변환되면 배열의 크기 정보가 사라지므로, 추가적인 크기 매개변수를 사용해야 합니다.
void process(int* data, size_t size) {
for (size_t i = 0; i < size; ++i) {
std::cout << data[i] << std::endl;
}
}
- 경계를 초과하는 접근 위험
- 유효한 크기 정보를 보장할 수 없기 때문에, 실수로 배열 범위를 초과하는 접근이 발생할 가능성이 큽니다.
int arr[5] = {1, 2, 3, 4, 5};
process(arr, 10); // 잘못된 크기 전달로 인해 경계 초과 발생 가능
- 메모리 안전성 문제
- 포인터를 사용하면 동적 할당된 메모리를 수동으로 관리해야 하며, 메모리 누수 및 접근 오류가 발생할 위험이 큽니다.
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
을 사용하면 크기 정보를 유지하면서 여러 유용한 기능을 사용할 수 있습니다.
- 배열과 컨테이너 참조
std::span
은 원시 배열,std::vector
,std::array
를 직접 참조할 수 있습니다.
- 부분 범위(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
- 정적 크기와 동적 크기 지원
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::span
을std::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); // 크기 전달 필수
}
이 방식에는 몇 가지 문제가 있습니다.
- 잘못된 크기 전달 가능
process(arr, 10);
과 같이 잘못된 크기를 전달하면 정의되지 않은 동작(UB)이 발생할 수 있습니다.
- 컨테이너 타입별로 별도 함수 필요
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::span
과 std::vector
, std::array
의 차이점을 비교하여 각각의 장단점을 살펴보겠습니다.
std::span과 std::vector, std::array 비교
C++에서는 배열과 컨테이너를 다루기 위해 여러 가지 방법이 존재합니다. 그중 대표적인 것이 std::vector
, std::array
, 그리고 C++20에서 새롭게 도입된 std::span
입니다. 이들은 모두 데이터를 저장하고 참조하는 기능을 제공하지만, 내부적인 동작 방식과 사용 목적에서 차이가 있습니다.
이번 장에서는 std::span
과 std::vector
, std::array
의 차이점을 비교하며, 각 방식의 장단점을 살펴보겠습니다.
std::span과 std::vector, std::array의 주요 차이점
특징 | std::span | std::vector | std::array |
---|---|---|---|
데이터 소유 여부 | X (참조) | O (소유) | O (소유) |
메모리 할당 | 없음 | 동적 할당 | 정적 할당 |
크기 조정 가능 여부 | X | O (resize() 가능) | X (컴파일 시 크기 고정) |
배열과 컨테이너 지원 | O (int arr[] , std::vector , std::array 모두 참조 가능) | X (자체 데이터만 저장) | X (자체 데이터만 저장) |
복사 비용 | 없음 (참조) | 있음 (복사 시 비용 발생) | 있음 (복사 시 비용 발생) |
부분 참조 가능 여부 | O (subspan() 지원) | X | X |
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::span
은 std::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
을 사용하는 것이 적절합니다.
- 배열과 컨테이너를 동일한 방식으로 다루고 싶을 때
- 함수에서
std::vector
,std::array
, 원시 배열을 한 번에 처리하고 싶다면std::span
이 유용합니다.
- 데이터 복사를 피해야 할 때
- 데이터를 소유하지 않고 참조만 하므로, 복사 비용을 줄일 수 있습니다.
- 부분 배열 참조가 필요할 때
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::vector
나std::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::span
은 const
키워드와 함께 사용하여 불변(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 뷰로 변환 후,rows
와cols
정보를 이용하여 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
을 표준 인터페이스로 활용하여 유지보수성을 높이고, 성능 개선 효과를 얻을 수 있을 것입니다.