이 요구 사항은 **C++**의 유형 열화 규칙과 컴파일 타임 삭제자 선택의 필요성에서 비롯됩니다. 배열 유형이 템플릿에 전달되면 포인터로 열화되어 배열 크기 정보가 제거되어 스칼라(delete)와 배열(delete[]) 해제를 구분할 수 없습니다. std::unique_ptr는 부분 템플릿 특수화를 통해 이를 해결합니다: 기본 템플릿 **std::unique_ptr<T>**는 스칼라 delete를 호출하는 **std::default_delete<T>**를 사용하며, **std::unique_ptr<T[]>**는 **delete[]**를 호출하는 **std::default_delete<T[]>**를 인스턴스화합니다. 이 명시적 문법은 컴파일러가 런타임 유형 탐색이나 오버헤드 없이 올바른 파괴 코드를 생성하도록 보장합니다.
맥락: 저지연 오디오 처리 엔진은 **new float[buffer_size]*로 할당된 float PCM 샘플 버퍼를 하드웨어 드라이버 API에서 수신합니다. 이러한 버퍼는 엄격한 실시간 제약과 예외 안전성을 유지하면서 디지털 신호 처리 필터 체인을 통과해야 합니다.
문제: 팀은 이러한 C 스타일 배열에 대해 RAII 안전성을 제공하는 스마트 포인터 솔루션이 필요했으나, std::vector의 크기/용량 추적 오버헤드를 도입하면 SIMD 작업의 캐시 라인 정렬 요구사항이 위반됩니다. 특히, 배열로 할당한 메모리에 대해 스칼라 delete를 사용하면 힙이 손상되고 오디오 파이프라인이 충돌할 수 있습니다.
수동 삭제가 포함된 원시 포인터. 이 접근법은 모든 종료 경로에서 명시적 delete[] 호출과 함께 나체 float* 포인터를 활용했습니다. 장점: 제로 추상화 오버헤드 및 하드웨어 API 호환성. 단점: 예외 안전하지 않음; 만약 필터가 처리 중 예외를 던지면 버퍼가 누수되며, 20개 필터 단계 전반에 걸쳐 올바른 삭제 논리를 유지하는 것은 관리 불가능해졌습니다. 생산에서의 신뢰성 위험 때문에 거절되었습니다.
std::vector<float> 컨테이너. 버퍼를 std::vector로 감싸면 자동 메모리 관리와 크기 추적을 제공했습니다. 장점: 예외 안전성과 범위 검사 가능성. 단점: std::vector는 암묵적으로 용량 포인터를 저장하므로 (일반적으로 24바이트 오버헤드) 오디오 하드웨어와의 고정 크기 DMA 정렬 계약이 깨집니다. 또한, std::vector는 변경 가능한 소유권과 잠재적 재배치를 가정하므로 드라이버의 고정 버퍼 풀이 충돌합니다.
std::unique_ptr<float[]> 특수화. 이 솔루션은 **std::unique_ptr<float[]>**를 사용하여 자동으로 **std::default_delete<float[]>**를 인스턴스화했습니다. 장점: 제로 오버헤드(크기 동일한 하나의 포인터), 보장된 delete[] 호출, 효율적인 필터 체인 핸드오프를 위한 이동 의미론, 및 복사를 방지하는 컴파일 타임 제약. 단점: 런타임 크기 정보 손실로 평행 추적이 필요하고, **std::make_unique<float[]>(size)**는 POD 유형에 대해 불필요할 수 있는 요소를 값 초기화합니다.
결정 및 결과. 우리는 **std::unique_ptr<float[]>**와 크기 추적을 위한 경량 스팬 유사 뷰를 결합하여 선택했습니다. 이는 하드웨어 정렬 제약을 위반하지 않으면서 예외 안전성을 제공했습니다. 이 시스템은 수개월 동안 메모리 누수 없이 오디오 스트림을 처리했으며, 명시적 배열 특수화는 개발자가 배열 생성 시 **std::unique_ptr<float>**를 시도했을 때 컴파일 중에 중요한 버그를 포착하여 런타임 이전에 올바른 문법을 강제로 적용했습니다.
**왜 **std::unique_ptr<Base[]>**가 **new Derived[N]**로 초기화되는 것을 거부하는가, **std::unique_ptr<Derived>**가 **std::unique_ptr<Base>로 변환되는 경우?
배열 유형은 단일 포인터와는 비공변행 동작을 나타냅니다. Derived는 포인터 조정을 통해 암묵적으로 Base로 변환되지만, **Derived[]**는 배열 인덱스 산술이 정적 유형 크기에 의존하기 때문에 **Base[]**로 변환할 수 없습니다; **Derived[]**의 i 요소에 접근하기 위한 **Base[]**의 뷰는 잘못된 바이트 오프셋을 계산할 것입니다. 따라서 std::unique_ptr의 배열 특수화는 잘못된 정렬된 메모리에 접근을 방지하기 위해 서로 다른 배열 유형 간의 변환 생성자를 명시적으로 삭제하며, 반면 스칼라 버전은 변환을 허용하여 안전성을 위해 가상 소멸자를 필요로 합니다.
**std::make_unique<T[]>(n)**이 요소를 어떻게 초기화하는지, **std::make_unique<T>(args...)와 비교하여 이로 인해 적용 가능성이 제한되는 이유는 무엇인가요?
배열 오버로드 **std::make_unique<T[]>(n)**는 모든 n 요소에 대해 값 초기화를 수행하여 스칼라를 0으로 초기화하거나 객체를 기본 생성합니다. 이는 인수가 T의 생성자에게 전달되는 스칼라 형식과 다릅니다. 이 구별은 개별 요소에 대한 생성자 인수를 전달할 수 없으므로 비기본 생성 가능 유형의 배열에 대해 std::make_unique 를 사용할 수 없습니다. 후보자들은 흔히 **std::make_unique<NonDefaultConstructible[]>(5, args)**를 시도하지만, 이는 컴파일 오류를 발생시켜 수동 루프나 std::vector 사용을 강제합니다.
std::unique_ptr<T>(스칼라)가 **new T[N]로 메모리를 관리할 때 어떤 정의되지 않은 동작이 발생하며, 컴파일러는 왜 침묵하는가?
스칼라 std::unique_ptr는 **std::default_delete<T>**를 사용하여 delete(스칼라 삭제)를 호출합니다. 이 경우 배열로 할당된 메모리에 대한 **new T[N]**에 적용하면 불일치가 발생하여 정의되지 않은 동작을 초래합니다. 일반적으로 첫 번째 요소의 메모리만 해제되거나 힙 할당자의 메타데이터가 손상됩니다. 컴파일러는 경고하지 않는데, 이는 템플릿 파라미터 T가 열화되기 때문입니다; **new T[N]**는 T*를 반환하며, std::unique_ptr 생성 시점에서 유형 시스템은 배열 구별을 잃게 됩니다. 이러한 침묵의 실패 모드는 **std::unique_ptr<T[]>**가 안전한 대안으로 존재하는 이유입니다.