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[]>를 선택하는 것이 바람직합니다.