[모던 C++ #2] C-Style 배열을 벗어나: std::array, std::vector 그리고 std::span으로!

과거 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을 사용하면 함수 호출 시 크기 정보 전달을 잊을 일이 없고, 복사 없이 컨테이너의 특정 범위를 참조할 수 있습니다.

왜 이런 변화가 필요한가?

  1. 안전성
    C-Style 배열 사용 시 발생할 수 있는 범위 초과 접근, 사이즈 파라미터 누락 등의 위험을 줄일 수 있습니다.
  2. 가독성과 유지보수성 개선
    std::array, std::vector, std::span을 사용하면 코드가 의도를 명확히 표현합니다. 예를 들어, std::span으로 함수에 배열을 전달하면 "이 함수는 배열을 참조하며, 그 크기도 알고 있다"는 사실이 명확히 드러납니다.
  3. 표준화된 방식 활용
    C++ 표준 컨테이너와 뷰 타입을 사용함으로써, 알고리즘 라이브러리(std::ranges나 STL 알고리즘)와도 쉽게 연계할 수 있습니다. 이는 개발 생산성과 코드 품질을 동시에 개선하는 데 기여합니다.
반응형