std::unique_ptr 동적 배열 관리 핵심 기법

RAII 패러다임과 스마트 포인터 배열 관리

C++에서 자원 관리의 핵심은 객체 생명주기와 자원 획득/해제를 결합하는 RAII 원칙입니다. 특히 동적 배열처럼 복잡한 자원을 다룰 때, 스마트 포인터를 활용하면 예외 상황에서도 안전한 메모리 해제를 보장합니다.

스마트 포인터 배열 특화 버전

std::unique_ptr는 템플릿 특화를 통해 배열 타입을 지원합니다. T[] 형태로 선언하면 내부적으로 delete[]를 호출하는 삭제자가 적용됩니다.

#include <memory>
#include <iostream>

int main() {
    // 배열 특화 버전으로 7개 요소 생성
    auto buffer = std::make_unique<double[]>(7);
    
    for (size_t idx = 0; idx < 7; ++idx) {
        buffer[idx] = idx * 3.14;
        std::cout << buffer[idx] << " ";
    }
    // 범위 종료 시 자동으로 delete[] 호출
}

배열 초기화와 생성 패턴

배열 삭제자의 중요성

일반 unique_ptr<T>delete를 사용하므로 배열에 적용 시 미정의 동작이 발생합니다. 반면 unique_ptr<T[]>는 배열 삭제자를 내장하여 올바른 메모리 해제를 보장합니다.

스마트 포인터 형태해제 연산용도
unique_ptr<T>delete단일 객체
unique_ptr<T[]>delete[]동적 배열

안전한 배열 생성

std::make_unique를 사용하면 예외 발생 시에도 자원 누수를 방지할 수 있습니다. 이 함수는 원자적 생성을 보장하여 메모리 할당과 스마트 포인터 래핑이 분리되지 않습니다.

// 위험: new[] 후 예외 발생 시 누수 가능
int* raw = new int[50];
mightThrow();  // 예외 발생 시 raw 해제 불가
delete[] raw;

// 안전: make_unique 사용
auto safeBuf = std::make_unique<char[]>(256);
mightThrow();  // 예외 발생해도 자동 해제

다차원 배열의 스마트 관리

연속 메모리 기반 다차원 배열은 단일 unique_ptr로 관리할 수 있습니다. 행렬 형태를 논리적으로 표현하려면 인덱스 계산식이 필요합니다.

template<typename T>
class CompactMatrix {
    std::unique_ptr<T[]> storage;
    size_t rowDim, colDim;
    
public:
    CompactMatrix(size_t rows, size_t cols) 
        : storage(std::make_unique<T[]>(rows * cols))
        , rowDim(rows), colDim(cols) {}
    
    T& at(size_t r, size_t c) {
        return storage[r * colDim + c];  // 행 우선 인덱싱
    }
    
    size_t rows() const { return rowDim; }
    size_t cols() const { return colDim; }
};

예외 안전성과 자원 해제

스택 해제 시 자동 정리

예외가 전파되면 스택 해제 과정에서 지역 객체의 소멸자가 자동 호출됩니다. unique_ptr는 이 메커니즘을 활용하여 어떤 경로로든 안전한 자원 회수를 실현합니다.

void processWithGuarantee() {
    auto resource = acquireResource();  // unique_ptr로 핑
    
    if (validationFailed()) {
        throw std::runtime_error("검증 실패");
        // 소멸자에서 자원 자동 반납
    }
    
    // 정상 흐름에서도 범위 종료 시 해제
}

성능 최적화 전략

불필요한 복사 제거

unique_ptr는 이동 전용 타입으로 복사 생성이 금지되어 있습니다. 이는 소유권 명확화와 함께 런타임 오버헤드를 최소화합니다.

std::unique_ptr<int[]> createDataset(size_t n) {
    auto data = std::make_unique<int[]>(n);
    initialize(data.get(), n);
    return data;  // NRVO 또는 이동 적용
}

// 사용처
auto myData = createDataset(1000);  // 이동 의미론 적용, 복사 없음

컨테이너 선택 기준

unique_ptr<T[]>std::vector의 선택은 요구사항에 따라 달라집니다:

  • unique_ptr 배열: 고정 크기, 최소 오버헤드, 소유권 전달 필요 시
  • vector: 동적 확장, 크기 조회, STL 알고리즘 호환 필요 시

사용자 정의 삭제자 활용

배열 해제 시 추가 로직이 필요하면 함수 객체 또는 람다로 삭제자를 지정할 수 있습니다. 템플릿 매개변수로 전달되어 성능 저하 없이 동작합니다.

struct TrackedDeleter {
    static size_t deallocCount;
    
    void operator()(int* ptr) const {
        ++deallocCount;
        std::cout << "배열 해제 #" << deallocCount << "\n";
        delete[] ptr;
    }
};

size_t TrackedDeleter::deallocCount = 0;

// 사용자 삭제자 적용
std::unique_ptr<int[], TrackedDeleter> monitored(new int[20]);

STL 알고리즘과의 연동

unique_ptr<T[]>는 직접적인 반복자를 제공하지 않으나, 원시 포인터를 활용해 알고리즘 라이브러리와 연동할 수 있습니다.

auto values = std::make_unique<int[]>(100);
std::iota(values.get(), values.get() + 100, 1);  // 1부터 채움

// 부분 정렬 적용
std::nth_element(values.get(), values.get() + 50, values.get() + 100);

현대적 C++ 자원 관리 방향

unique_ptr<T[]>는 안전한 동적 배열 관리의 중간 단계입니다. 대부분의 응용 프로그램에서는 std::vector가 더 적합하며, std::span(C++20)은 뷰 기반 접근을 제공합니다.

// 권장: vector를 기본 선택
std::vector<float> flexible(100);
flexible.push_back(3.14f);  // 확장 가능

// 소유권 분리가 필요한 경우
auto owned = std::make_unique<std::vector<float>>(100);
auto view = std::span<float>(owned->data(), owned->size());

새 프로젝트에서는 std::vector를 우선 고려하고, C 인터페이스 호환성이나 특수한 소유권 전달이 필요할 때 unique_ptr<T[]>를 선택하는 것이 바람직합니다.

태그: std::unique_ptr RAII 동적배열 스마트포인터 C++11

7월 14일 00:42에 게시됨