과거 C++98/03 시절, 정적 크기를 갖는 C-Style 배열은 흔하게 사용되었지만, 실제로는 다양한 문제를 안고 있었습니다. 배열의 크기를 코드 곳곳에 명시해야 하거나, 함수 인자로 전달할 때 크기 정보를 잃기 쉽고, 범위 초과 액세스를 검증하기 어렵다는 점 등이 대표적입니다. 이러한 방식은 대규모 프로젝트에서 메모리 안전성을 저해하고, 유지보수를 어렵게 만들었습니다.
모던 C++에서는 이 문제를 해결하기 위해 여러 대안들이 등장했습니다. std::array는 정적 크기 배열을 안전하게 감싸며, std::vector는 동적 크기 배열을 편리하게 관리하고, C++20의 std::span은 기존 배열이나 컨테이너의 뷰(view) 역할을 하여 범위 정보를 담은 인터페이스를 제공합니다.
관련 참고 자료:
과거: C-Style 배열과 그 한계
C-Style 배열은 선언 시 크기가 고정되고, 범위 안전장치가 전혀 없습니다. 또한 함수 인자로 배열을 전달하면 사실상 포인터로 변환되어 배열 크기 정보를 잃게 됩니다.
#include <iostream>
void print_array(const int* arr, std::size_t size) {
for (std::size_t i = 0; i < size; ++i)
std::cout << arr[i] << " ";
std::cout << "\n";
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
// 함수 호출 시 크기를 따로 전달해야 함
print_array(arr, 5);
return 0;
}
위 예제처럼 배열을 함수에 넘길 때 항상 크기를 함께 전달해야 하며, 이것이 누락되거나 잘못 설정되면 런타임 에러로 이어질 수 있습니다. 또한 범위 초과 접근을 제어할 수 없고, 이는 심각한 버그를 야기할 수 있습니다.
현재: std::array, std::vector, 그리고 std::span
std::array: 정적 크기 배열의 안전한 래퍼
std::array는 정적 크기 배열을 STL 컨테이너로 래핑한 형태로, 크기 정보를 타입에 포함하고 있습니다. 이는 템플릿 인자로 배열 크기가 타입에 들어가기 때문에, 언제나 크기 정보와 함께 움직이며 범위 체크 함수(.at())도 제공합니다.
#include <array>
#include <iostream>
int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto v : arr) {
std::cout << v << " ";
}
std::cout << "\n";
// 범위 초과 접근 시 예외 발생
// std::cout << arr.at(10) << "\n"; // std::out_of_range 예외
return 0;
}
std::array는 단순히 C-Style 배열을 래핑한 것이지만, STL 컨테이너와 동일한 인터페이스를 제공하기 때문에 알고리즘 라이브러리와 자연스럽게 연계할 수 있습니다.
std::vector: 동적 크기 변경이 가능한 배열
동적 배열이 필요한 경우 std::vector가 정석적인 선택입니다. 동적으로 크기를 조정(push_back, resize)할 수 있고, 필요에 따라 예약(reserve)을 통해 메모리 재할당 최소화도 가능합니다.
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
vec.push_back(5);
for (auto v : vec) {
std::cout << v << " ";
}
std::cout << "\n"; // 1 2 3 4 5
return 0;
}
std::vector는 대다수 상황에서 기본적인 배열 대체물로 활용할 수 있으며, 확장성, 안전성, 편의성을 모두 제공합니다.
std::span: 복사 없이 배열, 벡터를 참조하는 뷰
C++20에서 도입된 std::span은 기존 배열이나 컨테이너에 대한 "비소유(non-owning)" 뷰를 제공합니다. std::span은 크기 정보를 함께 보유하여, 함수 인자로 전달할 때 별도의 크기 파라미터를 넘기지 않아도 됩니다. 이는 범위 기반 연산을 안전하고 편리하게 수행할 수 있게 합니다.
#include <array>
#include <vector>
#include <span>
#include <iostream>
void print_span(std::span<int> s) {
for (auto val : s)
std::cout << val << " ";
std::cout << "\n";
}
int main() {
std::array<int,5> arr = {1,2,3,4,5};
std::vector<int> vec = {10,20,30,40,50};
print_span(arr); // 크기 정보 자동 전달
print_span(vec); // vector도 동일하게 처리 가능
// 배열의 일부분만 뷰로 만들 수도 있음
print_span(std::span<int>(vec).subspan(1,3)); // 20,30,40 출력
return 0;
}
std::span을 사용하면 함수 호출 시 크기 정보 전달을 잊을 일이 없고, 복사 없이 컨테이너의 특정 범위를 참조할 수 있습니다.
왜 이런 변화가 필요한가?
- 안전성
C-Style 배열 사용 시 발생할 수 있는 범위 초과 접근, 사이즈 파라미터 누락 등의 위험을 줄일 수 있습니다. - 가독성과 유지보수성 개선
std::array, std::vector, std::span을 사용하면 코드가 의도를 명확히 표현합니다. 예를 들어, std::span으로 함수에 배열을 전달하면 "이 함수는 배열을 참조하며, 그 크기도 알고 있다"는 사실이 명확히 드러납니다. - 표준화된 방식 활용
C++ 표준 컨테이너와 뷰 타입을 사용함으로써, 알고리즘 라이브러리(std::ranges나 STL 알고리즘)와도 쉽게 연계할 수 있습니다. 이는 개발 생산성과 코드 품질을 동시에 개선하는 데 기여합니다.
'개발 이야기 > C++' 카테고리의 다른 글
[모던 C++ #4] 옛날 for 루프를 버리고 range-based for와 C++20 Ranges로! (0) | 2024.12.14 |
---|---|
[모던 C++ #3] 매크로 상수를 버리고 constexpr와 enum class로! (0) | 2024.12.14 |
[모던 C++ #1] 이제는 버려야 할 레거시: auto_ptr를 내려놓고 unique_ptr로! (0) | 2024.12.14 |
[C++23 새기능 소개] std::ranges::sample (0) | 2024.12.12 |
[C++23 새기능 소개] std::in_place_stop_source와 std::in_place_stop_token (0) | 2024.12.12 |