[2026] C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열

[2026] C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열

이 글의 핵심

C++ unique_ptr 기초는 알겠는데, C API 연동·배열·Pimpl 패턴·이동 시맨틱스는 어떻게 쓰나요? 커스텀 삭제자, 배열 지원, Pimpl 구현, 이동 의미론, 자주 하는 실수, 프로덕션 패턴까지. 문제 시나리오로 시작하는 unique_ptr 고급 실전 가이드.

들어가며: unique_ptr 기초는 알겠는데, 실전에서는?

”C API의 malloc/free를 C++에서 어떻게 안전하게 쓰나요?”

unique_ptr 기초를 익혔다면 make_unique, std::move, get() 정도는 알고 있을 겁니다. 그런데 실제 프로젝트에서는 C 라이브러리 연동(malloc/free, FILE*, HANDLE), Pimpl 패턴으로 구현 세부사항 숨기기, 동적 배열 관리, 이동 의미론을 활용한 팩토리 패턴 등이 필요합니다. 이 글에서는 unique_ptr의 고급 기능을 문제 시나리오부터 완전한 예제, 자주 하는 실수, 프로덕션 패턴까지 다룹니다. 비유하면: unique_ptr 기초는 “열쇠를 지갑에 넣어 두면 나갈 때 자동으로 문이 잠기는 것”이라면, 고급 기능은 “다양한 종류의 문(파일, 소켓, C API 버퍼)에 맞는 자동 잠금 장치를 설치하는 것”입니다. 이 글을 읽으면:

  • 커스텀 삭제자로 C API·파일 핸들·소켓 등을 RAII로 안전하게 관리할 수 있습니다.
  • unique_ptr로 동적 배열을 다루고, std::vector와의 선택 기준을 알 수 있습니다.
  • Pimpl 패턴으로 ABI 안정성을 확보하는 방법을 익힐 수 있습니다.
  • 이동 의미론과 unique_ptr을 조합한 팩토리·컨테이너 패턴을 적용할 수 있습니다.
  • 자주 하는 실수와 프로덕션 체크리스트를 활용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

C++ 개발자로서의 여정

C++은 제게 사랑과 증오의 대상입니다. 저수준 제어의 쾌감과 동시에 세그멘테이션 폴트의 공포를 동시에 안겨주는 언어죠. 대학 시절 처음 포인터를 배울 때는 “왜 이렇게 복잡하게 만들었을까?” 싶었지만, 실무에서 메모리 최적화가 필요한 순간이 오니 그 이유를 알겠더군요. 특히 기억에 남는 건 게임 엔진 프로젝트였습니다. 프레임 드롭 문제를 해결하려고 프로파일러를 돌려보니, 불필요한 복사 생성자 호출이 병목이었습니다. 이동 시맨틱과 완벽한 전달을 적용하자 FPS가 30에서 60으로 뛰어올랐죠. 그 순간 “C++의 복잡함은 이런 최적화를 위한 것이구나” 하고 깨달았습니다. 이 글에서는 이론만이 아니라, 실제로 부딪히며 배운 경험들을 공유하겠습니다.

목차

  1. 문제 시나리오
  2. 커스텀 삭제자 완전 가이드
  3. unique_ptr과 배열
  4. Pimpl 패턴과 unique_ptr
  5. 이동 의미론과 unique_ptr
  6. 완전한 고급 예제
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례와 선택 가이드
  9. 프로덕션 패턴
  10. 성능·체크리스트

1. 문제 시나리오

시나리오 1: “C API가 malloc으로 할당한 버퍼를 반환하는데, free를 언제 호출해야 할지 모르겠어요”

"libpng, libjpeg 같은 C 라이브러리가 malloc으로 버퍼를 반환해요."
"예외가 나거나 early return이 있으면 free를 깜빡하기 쉽습니다."

상황: char* buf = (char*)malloc(1024);로 받은 버퍼를 C++ 코드에서 사용할 때, 여러 경로(예외, early return)에서 free를 일일이 호출하기 어렵습니다. 한 곳이라도 빠지면 메모리 누수가 발생합니다. 해결 포인트: unique_ptr커스텀 삭제자free를 지정하면 RAII로 스코프를 벗어날 때 자동 해제됩니다.

시나리오 2: “FILE*를 열었는데 fclose를 어디서 호출해야 할지 헷갈려요”

"파일을 열고 파싱하다가 예외가 나면 fclose가 호출되지 않아요."
"여러 함수에서 파일을 넘겨다니다 보니 누가 닫을지 모르겠어요."

상황: FILE* fp = fopen("data.txt", "r");로 연 파일을 여러 함수에 전달할 때, “마지막으로 사용하는 쪽”이 fclose를 해야 하는데 그 시점을 추적하기 어렵습니다. 해결 포인트: unique_ptr<FILE, FileDeleter>로 감싸면 소유권이 명확해지고, 스코프를 벗어날 때 자동으로 fclose가 호출됩니다.

시나리오 3: “헤더를 수정하면 모든 사용자가 다시 컴파일해야 해요”

"Widget 클래스의 private 멤버를 바꿨는데, 이걸 include하는 100개 파일이 전부 재컴파일돼요."
"빌드 시간이 너무 길어요."

상황: 헤더에 구현 세부사항(private 멤버, 의존성)이 노출되면, 구현 변경 시마다 해당 헤더를 include하는 모든 코드가 재컴파일됩니다. 해결 포인트: Pimpl 패턴으로 구현을 unique_ptr<Impl>만 두고, .cpp 파일에 숨기면 헤더 변경 없이 구현만 수정할 수 있어 빌드 시간이 크게 줄어듭니다.

시나리오 4: “new[]로 할당한 배열을 unique_ptr로 관리하고 싶어요”

"동적 배열이 필요한데, shared_ptr은 배열 지원이 제한적이에요."
"unique_ptr<int[]>로 배열을 만들 수 있다고 들었는데, 어떻게 쓰나요?"

상황: new int[n]으로 할당한 배열을 unique_ptr로 관리하고 싶을 때, unique_ptr<T[]> 특수화와 make_unique<T[]>(n) 사용법을 알아야 합니다. 해결 포인트: std::unique_ptr<int[]> arr = std::make_unique<int[]>(100);으로 배열을 생성하고, arr[i]로 접근합니다. std::vector가 대부분의 경우 더 나은 선택이지만, C API 연동이나 고정 크기 배열이 필요할 때 유용합니다.

시나리오 5: “팩토리 함수가 new로 만든 객체를 반환하는데, 이동이 제대로 되는지 확인하고 싶어요”

"createWidget()이 new Widget()을 반환하는데, 호출자가 delete를 잊으면 누수돼요."
"unique_ptr로 반환하면 이동이 되는지, RVO가 적용되는지 궁금해요."

상황: 팩토리 함수가 힙에 할당한 객체를 반환할 때, 이동 의미론과 RVO가 어떻게 적용되는지 이해해야 합니다. 해결 포인트: std::unique_ptr<Widget> createWidget()으로 반환하면 소유권이 명확해지고, RVO 또는 이동으로 복사 없이 전달됩니다. 호출자가 받은 unique_ptr이 스코프를 벗어날 때 자동 해제됩니다.

2. 커스텀 삭제자 완전 가이드

커스텀 삭제자란?

unique_ptr의 기본 삭제자는 delete를 호출합니다. 하지만 C API의 free, fclose, CloseHandle다른 해제 함수가 필요한 리소스는 커스텀 삭제자를 지정해야 합니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart LR
    subgraph Default[기본 삭제자]
        D1[unique_ptrT]
        D2["delete ptr"]
    end
    subgraph Custom[커스텀 삭제자]
        C1["unique_ptrT, Deleter"]
        C2[Deleter(ptr)]
    end
    D1 --> D2
    C1 --> C2

C API: malloc/free

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <cstdlib>
#include <iostream>
int main() {
    // ✅ 권장: decltype(&std::free)로 삭제자 타입 지정
    auto buf = std::unique_ptr<char, decltype(&std::free)>(
        static_cast<char*>(std::malloc(1024)),
        &std::free
    );
    if (!buf) {
        throw std::bad_alloc();
    }
    // 버퍼 사용
    buf[0] = 'A';
    buf[1] = '\0';
    std::cout << buf.get() << std::endl;
    return 0;
}  // 소멸 시 free 자동 호출

코드 설명:

  • decltype(&std::free): free 함수 포인터의 타입을 지정합니다. void (*)(void*) 형태입니다.
  • 두 번째 인자 &std::free: 실제 호출할 삭제 함수를 전달합니다.
  • static_cast<char*>: mallocvoid*를 반환하므로 char*로 변환합니다.
  • 스코프를 벗어나면 소멸자가 free(buf.get())를 호출합니다.

람다를 이용한 삭제자 (상태 있는 삭제자)

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
int main() {
    auto deleter =  {
        std::cout << "Custom delete: " << *p << std::endl;
        delete p;
    };
    std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
    return 0;
}  // "Custom delete: 42" 출력 후 delete 호출

주의: 람다를 삭제자로 쓰면 unique_ptr의 크기가 증가할 수 있습니다(람다 캡처에 따라). 상태 없는 함수 포인터는 8바이트 추가, 상태 있는 람다는 더 커질 수 있습니다.

FILE* 래퍼 (파일 핸들)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <cstdio>
#include <stdexcept>
#include <string>
struct FileDeleter {
    void operator()(FILE* fp) const {
        if (fp) {
            std::fclose(fp);
        }
    }
};
using FileHandle = std::unique_ptr<FILE, FileDeleter>;
FileHandle openFile(const char* path, const char* mode) {
    FILE* fp = std::fopen(path, mode);
    if (!fp) {
        throw std::runtime_error(std::string("Cannot open: ") + path);
    }
    return FileHandle(fp);
}
int main() {
    auto file = openFile("data.txt", "r");
    if (file) {
        char buf[256];
        while (std::fgets(buf, sizeof(buf), file.get())) {
            // 처리...
        }
    }
    return 0;
}  // fclose 자동 호출

코드 설명:

  • FileDeleter: operator()를 정의한 함수 객체입니다. fp가 null이 아닐 때만 fclose를 호출합니다.
  • using FileHandle: 타입 별칭으로 가독성을 높입니다.
  • openFile: fopen으로 연 파일을 FileHandle로 감싸 반환합니다. 예외 시에도 RAII로 fclose가 호출됩니다.

Windows HANDLE (예시)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#ifdef _WIN32
#include <memory>
#include <windows.h>
struct HandleDeleter {
    void operator()(HANDLE h) const {
        if (h != nullptr && h != INVALID_HANDLE_VALUE) {
            CloseHandle(h);
        }
    }
};
using UniqueHandle = std::unique_ptr<void, HandleDeleter>;
UniqueHandle openMutex(const wchar_t* name) {
    HANDLE h = OpenMutexW(MUTEX_ALL_ACCESS, FALSE, name);
    if (!h) {
        throw std::runtime_error("OpenMutex failed");
    }
    return UniqueHandle(h);
}
#endif

삭제자 타입과 unique_ptr 크기

삭제자 종류unique_ptr 크기 (64비트)비고
기본 (delete)8 bytesraw 포인터와 동일
함수 포인터16 bytes포인터 + 삭제자
상태 없는 람다8 bytes빈 클래스 최적화
상태 있는 람다16+ bytes캡처 크기에 따라 증가

3. unique_ptr과 배열

unique_ptr<T[]> 특수화

unique_ptrT[] 배열 타입에 대한 특수화가 있습니다. 배열의 경우 delete[]를 호출해야 하므로, unique_ptr<T[]>delete[]를 사용합니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

flowchart TB
    subgraph Single[unique_ptrT]
        S1["delete ptr"]
    end
    subgraph Array["\"unique_ptrT(\"]>"]
        A1[""delete("] ptr"]
    end

배열 생성과 접근

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
int main() {
    // C++14: make_unique<T[]>(size)
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(100);
    arr[0] = 10;
    arr[99] = 20;
    for (int i = 0; i < 100; ++i) {
        arr[i] = i;
    }
    std::cout << arr[0] << " " << arr[99] << std::endl;  // 0 99
    return 0;
}  // delete[] 자동 호출

주의: unique_ptr<T[]>operator*(단일 객체 역참조)를 제공하지 않습니다. arr[i]로만 접근합니다.

배열 vs std::vector 선택 기준

상황권장이유
크기가 가변std::vectorresize, push_back 등 편의 기능
C API에 raw 포인터 전달std::vector 또는 unique_ptr<T[]>vec.data() 또는 arr.get()
고정 크기, 스택에 두기 어려움unique_ptr<T[]> 또는 std::array단순함
다형성 배열 (기반 클래스 포인터 배열)std::vector<std::unique_ptr<Base>>unique_ptr<T[]>는 다형성에 부적합

C API 연동: 고정 크기 버퍼

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <cstring>
#include <iostream>
void processWithBuffer(size_t size) {
    auto buf = std::make_unique<char[]>(size);
    std::memset(buf.get(), 0, size);
    // C API에 전달
    some_c_function(buf.get(), size);
    // 사용...
}  // delete[] 자동 호출

초기화된 배열 (C++20)

C++20에서는 std::make_unique<int[]>(10)이 0으로 초기화됩니다. C++14/17에서는 초기화가 보장되지 않을 수 있으므로, 명시적으로 초기화하려면: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// C++14/17: 수동 초기화
auto arr = std::make_unique<int[]>(100);
std::fill(arr.get(), arr.get() + 100, 0);
// 또는 std::vector 사용 (자동 0 초기화)
std::vector<int> vec(100, 0);

4. Pimpl 패턴과 unique_ptr

Pimpl이란?

Pimpl(Pointer to Implementation)은 구현 세부사항을 불투명 포인터로 숨겨, 헤더 변경 없이 구현만 수정할 수 있게 하는 패턴입니다. unique_ptr과 결합하면 ABI 안정성과 빌드 시간 단축을 얻을 수 있습니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TB
    subgraph Header[widget.h]
        W["class Widget"]
        P["unique_ptrImpl pImpl_"]
    end
    subgraph Impl[widget.cpp]
        I["class Widget Impl"]
        D["구현 세부사항"]
    end
    W --> P
    P -.->|포인터| I
    I --> D

기본 Pimpl 구현

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// widget.h
#pragma once
#include <memory>
class Widget {
public:
    Widget();
    ~Widget();
    Widget(Widget&&) noexcept = default;
    Widget& operator=(Widget&&) noexcept = default;
    // 복사는 명시적으로 구현 (Impl 복제 필요)
    Widget(const Widget&);
    Widget& operator=(const Widget&);
    void doSomething();
    int getValue() const;
private:
    class Impl;
    std::unique_ptr<Impl> pImpl_;
};

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// widget.cpp
#include "widget.h"
#include <vector>
#include <string>
// Impl 정의: 헤더에 노출되지 않음
class Widget::Impl {
public:
    std::vector<int> data;
    std::string name;
    void doSomethingInternal() {
        // 복잡한 구현...
    }
    int getValueInternal() const {
        return 42;
    }
};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
// 소멸자는 .cpp에 반드시 정의 (Impl이 완전 타입이어야 함)
Widget::~Widget() = default;
Widget::Widget(const Widget& other) : pImpl_(std::make_unique<Impl>(*other.pImpl_)) {}
Widget& Widget::operator=(const Widget& other) {
    if (this != &other) {
        pImpl_ = std::make_unique<Impl>(*other.pImpl_);
    }
    return *this;
}
void Widget::doSomething() {
    pImpl_->doSomethingInternal();
}
int Widget::getValue() const {
    return pImpl_->getValueInternal();
}

Pimpl의 핵심: 소멸자 정의 위치

반드시 소멸자를 .cpp 파일에 정의해야 합니다. 헤더에 ~Widget() = default;만 두면, 컴파일러가 unique_ptr<Impl>의 소멸자를 인스턴스화할 때 Impl완전 타입(complete type)이어야 하는데, 헤더에서는 Impl이 전방 선언만 되어 있어 불완전 타입이므로 컴파일 에러가 발생합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 예: 헤더에 소멸자 default
class Widget {
    // ...
    ~Widget() = default;  // Impl이 불완전 타입일 때 unique_ptr 소멸자 인스턴스화 실패!
};
// ✅ 올바른 예: .cpp에 소멸자 정의
// widget.cpp
Widget::~Widget() = default;  // 이 시점에 Impl은 완전 타입

이동 생성자/대입과 Pimpl

unique_ptr은 이동 가능하므로, Widget의 이동 생성자/대입은 = default로 두면 됩니다. Impl이 완전 타입일 필요는 없습니다(이동 시 포인터만 바꾸므로).

5. 이동 의미론과 unique_ptr

unique_ptr은 “이동 전용” 타입

unique_ptr은 복사가 삭제되어 있고, 이동만 가능합니다. 이는 “독점 소유권”을 타입 시스템으로 강제하는 설계입니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant A as ptr1
    participant B as ptr2
    participant H as Heap
    A->>H: 소유
    Note over A,B: std::move(ptr1)
    A->>B: 소유권 이전
    B->>H: 소유
    Note over A: nullptr

이동 시맨틱스 기본

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <cassert>
int main() {
    auto ptr1 = std::make_unique<int>(42);
    // ❌ 복사 불가
    // auto ptr2 = ptr1;  // 컴파일 에러
    // ✅ 이동
    auto ptr2 = std::move(ptr1);
    assert(!ptr1);           // ptr1은 nullptr
    assert(ptr2 && *ptr2 == 42);
    // 이동 대입
    auto ptr3 = std::make_unique<int>(100);
    ptr2 = std::move(ptr3);  // ptr2가 가리키던 42는 delete, ptr3의 100 소유권 이전
    assert(!ptr3);
    assert(ptr2 && *ptr2 == 100);
    return 0;
}

함수 인자: 소유권 이전

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
void takeOwnership(std::unique_ptr<int> ptr) {
    std::cout << *ptr << std::endl;
    // 함수 종료 시 ptr 소멸 → delete
}
int main() {
    auto ptr = std::make_unique<int>(42);
    takeOwnership(std::move(ptr));  // 소유권 이전
    // ptr은 nullptr, 사용 불가
    return 0;
}

함수 반환: RVO와 이동

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
std::unique_ptr<int> createValue() {
    return std::make_unique<int>(42);
    // RVO 또는 이동으로 반환 (복사 없음)
}
std::unique_ptr<int> createConditionally(bool flag) {
    if (flag) {
        return std::make_unique<int>(1);
    }
    return std::make_unique<int>(2);
    // 두 경로 모두 이동
}
int main() {
    auto p1 = createValue();
    auto p2 = createConditionally(true);
    return 0;
}

컨테이너에 unique_ptr 저장

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
#include <iostream>
struct Shape {
    virtual void draw() const = 0;
    virtual ~Shape() = default;
};
struct Circle : Shape {
    void draw() const override { std::cout << "Circle\n"; }
};
struct Rectangle : Shape {
    void draw() const override { std::cout << "Rectangle\n"; }
};
int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Rectangle>());
    // 또는
    shapes.emplace_back(std::make_unique<Circle>());
    for (const auto& s : shapes) {
        s->draw();
    }
    return 0;
}

이동과 vector: push_back(std::move(ptr))로 이동하여 넣습니다. emplace_back은 인자를 전달해 컨테이너 내부에서 직접 생성할 수 있습니다.

6. 완전한 고급 예제

예제 1: C API 래퍼 (libpng 스타일)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
struct FreeDeleter {
    void operator()(void* p) const {
        std::free(p);
    }
};
using UniqueBuffer = std::unique_ptr<char, FreeDeleter>;
UniqueBuffer allocateBuffer(size_t size) {
    void* p = std::malloc(size);
    if (!p) {
        throw std::bad_alloc();
    }
    return UniqueBuffer(static_cast<char*>(p));
}
int main() {
    auto buf = allocateBuffer(4096);
    std::memset(buf.get(), 0, 4096);
    // 사용...
    return 0;
}

예제 2: Pimpl + 팩토리

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// document.h
#pragma once
#include <memory>
#include <string>
class Document {
public:
    static std::unique_ptr<Document> create(const std::string& path);
    ~Document();
    void save();
    void load();
private:
    class Impl;
    std::unique_ptr<Impl> pImpl_;
    Document(std::unique_ptr<Impl> impl);
};

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// document.cpp
#include "document.h"
#include <fstream>
#include <iostream>
class Document::Impl {
public:
    std::string path;
    std::string content;
    void saveInternal() {
        std::ofstream f(path);
        f << content;
    }
    void loadInternal() {
        std::ifstream f(path);
        content.assign(std::istreambuf_iterator<char>(f), {});
    }
};
std::unique_ptr<Document> Document::create(const std::string& path) {
    auto impl = std::make_unique<Impl>();
    impl->path = path;
    impl->loadInternal();
    return std::unique_ptr<Document>(new Document(std::move(impl)));
}
Document::Document(std::unique_ptr<Impl> impl) : pImpl_(std::move(impl)) {}
Document::~Document() = default;
void Document::save() {
    pImpl_->saveInternal();
}
void Document::load() {
    pImpl_->loadInternal();
}

예제 3: 배열 + 커스텀 삭제자 (알려진 크기)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <iostream>
struct ArrayDeleter {
    size_t count;
    ArrayDeleter(size_t n) : count(n) {}
    void operator()(int* p) const {
        delete[] p;
    }
};
int main() {
    std::unique_ptr<int, ArrayDeleter> arr(new int[10], ArrayDeleter(10));
    for (int i = 0; i < 10; ++i) {
        arr.get()[i] = i;
    }
    return 0;
}

참고: unique_ptr<int[]>를 쓰면 ArrayDeleter 없이 delete[]가 자동 호출됩니다. 커스텀 삭제자가 필요한 경우(예: mmap/munmap)에만 위 패턴을 사용합니다.

예제 4: 다형성 + unique_ptr 이동

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <memory>
#include <vector>
#include <iostream>
struct Animal {
    virtual void speak() const = 0;
    virtual ~Animal() = default;
};
struct Dog : Animal {
    void speak() const override { std::cout << "Woof\n"; }
};
struct Cat : Animal {
    void speak() const override { std::cout << "Meow\n"; }
};
std::unique_ptr<Animal> createAnimal(const std::string& type) {
    if (type == "dog") return std::make_unique<Dog>();
    if (type == "cat") return std::make_unique<Cat>();
    return nullptr;
}
int main() {
    std::vector<std::unique_ptr<Animal>> zoo;
    zoo.push_back(createAnimal("dog"));
    zoo.push_back(createAnimal("cat"));
    for (const auto& a : zoo) {
        a->speak();
    }
    return 0;
}

7. 자주 발생하는 에러와 해결법

에러 1: get()으로 얻은 포인터에 delete 적용

증상: 이중 해제(double-free)로 크래시.

// ❌ 잘못된 코드
auto ptr = std::make_unique<int>(42);
delete ptr.get();  // 이중 해제! unique_ptr 소멸 시 또 delete

해결법: get()non-owning 참조만 반환합니다. delete 금지. C API에 넘길 때만 사용.

// ✅ 올바른 코드
auto ptr = std::make_unique<int>(42);
some_c_api(ptr.get());  // 읽기/쓰기만, 소유권은 ptr에 유지

에러 2: Pimpl 소멸자를 헤더에 default로 두기

증상: error: invalid application of 'sizeof' to incomplete type 'Widget::Impl' 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 코드 (widget.h)
class Widget {
    class Impl;
    std::unique_ptr<Impl> pImpl_;
public:
    ~Widget() = default;  // Impl이 불완전 타입 → 에러
};

해결법: 소멸자를 .cpp에 정의합니다.

// ✅ 올바른 코드 (widget.cpp)
Widget::~Widget() = default;

에러 3: unique_ptr<T[]>에 operator* 사용

증상: unique_ptr<int[]>에는 operator*가 없습니다.

// ❌ 잘못된 코드
auto arr = std::make_unique<int[]>(10);
int x = *arr;  // 컴파일 에러

해결법: arr[i] 또는 arr.get()[i]를 사용합니다.

// ✅ 올바른 코드
int x = arr[0];

에러 4: 이동 후 원본 사용

증상: nullptr 역참조 또는 정의되지 않은 동작. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 코드
auto ptr1 = std::make_unique<int>(42);
auto ptr2 = std::move(ptr1);
std::cout << *ptr1 << std::endl;  // ptr1은 nullptr!

해결법: 이동 후 원본은 사용하지 않습니다. 필요하면 이동 전에 복사본을 만들거나, shared_ptr을 고려합니다.

에러 5: 커스텀 삭제자 없이 malloc 버퍼를 unique_ptr에 넣기

증상: delete가 호출되는데, malloc으로 할당했으므로 free를 써야 함 → 정의되지 않은 동작.

// ❌ 잘못된 코드
std::unique_ptr<char> buf(static_cast<char*>(std::malloc(100)));
// 소멸 시 delete 호출 → 잘못됨! free여야 함

해결법: 커스텀 삭제자로 free를 지정합니다.

// ✅ 올바른 코드
auto buf = std::unique_ptr<char, decltype(&std::free)>(
    static_cast<char*>(std::malloc(100)), &std::free);

에러 6: unique_ptr을 복사로 함수에 전달

증상: 컴파일 에러. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 코드
void take(std::unique_ptr<int> p) {}
auto ptr = std::make_unique<int>(42);
take(ptr);  // 복사 불가!

해결법: std::move(ptr)로 전달합니다.

// ✅ 올바른 코드
take(std::move(ptr));

에러 7: 배열이 아닌 unique_ptr에 delete[] 사용

증상: new[]로 할당한 배열을 unique_ptr<T>에 넣으면 delete만 호출됨 → 정의되지 않은 동작.

// ❌ 잘못된 코드
std::unique_ptr<int> arr(new int[10]);  // delete 호출됨, delete[] 아님!

해결법: unique_ptr<int[]>를 사용합니다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 올바른 코드
std::unique_ptr<int[]> arr(new int[10]);
// 또는
auto arr = std::make_unique<int[]>(10);

8. 모범 사례와 선택 가이드

선택 플로우차트

다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

flowchart TD
    A[리소스 타입은?] --> B[힙 객체 new/delete]
    A --> C[C API malloc/free 등]
    A --> D[파일/핸들 등]
    B --> E[단일 객체?]
    E -->|Yes| F[unique_ptr T]
    E -->|No| G[배열]
    G --> H["unique_ptr T 또는 vector"]
    C --> I[커스텀 삭제자 + free]
    D --> J[커스텀 삭제자 + fclose/CloseHandle 등]
    F --> K[make_unique 사용]
    H --> L["make_unique T 또는 vector"]

모범 사례 요약

규칙설명
make_unique 사용new 직접 사용 지양
C API는 커스텀 삭제자malloc → free, fopen → fclose 등
Pimpl 소멸자는 .cpp에Impl이 완전 타입이어야 함
배열은 unique_ptr<T[]>delete[] 자동 호출
get()에 delete 금지non-owning 참조만
소유권 이전은 std::move복사 불가

unique_ptr vs vector (배열)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 고정 크기, C API 연동
auto buf = std::make_unique<char[]>(1024);
c_api_read(buf.get(), 1024);
// 가변 크기, 일반적인 경우
std::vector<int> vec;
vec.resize(100);

unique_ptr vs raw 포인터 (소유권 표현)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 소유권 있음: unique_ptr
class Owner {
    std::unique_ptr<Resource> resource_;
};
// ✅ 소유권 없음 (참조만): raw 포인터 또는 참조
void process(const Resource* r);
void process(const Resource& r);

9. 프로덕션 패턴

패턴 1: 팩토리에서 unique_ptr 반환

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

class WidgetFactory {
public:
    static std::unique_ptr<Widget> create(const std::string& type) {
        if (type == "A") return std::make_unique<WidgetA>();
        if (type == "B") return std::make_unique<WidgetB>();
        return nullptr;
    }
};

패턴 2: Pimpl + ABI 안정성

헤더에 unique_ptr<Impl>만 두고 구현을 .cpp에 숨기면, 라이브러리 구현을 바꿔도 바이너리 호환성을 유지할 수 있습니다. 사용자 코드 재컴파일 없이 .so/.dll만 교체 가능합니다.

패턴 3: C API 래퍼 클래스

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class CBufferGuard {
    std::unique_ptr<char, decltype(&std::free)> buf_;
public:
    explicit CBufferGuard(size_t size)
        : buf_(static_cast<char*>(std::malloc(size)), &std::free) {
        if (!buf_) throw std::bad_alloc();
    }
    char* get() { return buf_.get(); }
    const char* get() const { return buf_.get(); }
};

패턴 4: 리소스 핸들 (RAII)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class FileGuard {
    std::unique_ptr<FILE, FileDeleter> file_;
public:
    explicit FileGuard(const char* path)
        : file_(std::fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    FILE* get() { return file_.get(); }
};

패턴 5: 옵셔널 소유 (nullable)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Parser {
    std::unique_ptr<Cache> cache_;  // 필요 시에만 생성
public:
    void enableCache() {
        cache_ = std::make_unique<Cache>();
    }
    void parse() {
        if (cache_) {
            cache_->lookup(/* ....*/);
        }
    }
};

패턴 6: 다형성 컨테이너 + 팩토리

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

std::vector<std::unique_ptr<Handler>> handlers;
handlers.push_back(HandlerFactory::create("http"));
handlers.push_back(HandlerFactory::create("websocket"));
for (const auto& h : handlers) {
    h->handle(request);
}

10. 성능·체크리스트

크기 및 오버헤드

구성unique_ptr 크기 (64비트)
기본 (delete)8 bytes
함수 포인터 삭제자16 bytes
상태 없는 람다 삭제자8 bytes (빈 클래스 최적화)

make_unique vs new

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 권장: 예외 안전, 한 줄
auto p = std::make_unique<Widget>(a, b);
// ⚠️ 구식: 예외 시 누수 가능 (C++17 이전)
std::unique_ptr<Widget> p(new Widget(a, b));

프로덕션 체크리스트

  • C API 연동 시 커스텀 삭제자 사용 (malloc→free, fopen→fclose)
  • Pimpl 소멸자는 반드시 .cpp에 정의
  • 배열은 unique_ptr<T[]> 또는 std::vector 사용
  • get()으로 얻은 포인터에 delete 금지
  • 소유권 이전 시 std::move 사용
  • 기본은 make_unique, make_unique<T[]>(n) 사용

마무리

핵심 요약

커스텀 삭제자: C API(malloc/free, FILE*), Windows HANDLE 등 RAII로 안전하게 관리
배열: unique_ptr<T[]> 또는 std::vector
Pimpl: 구현 숨김, ABI 안정성, 소멸자는 .cpp에 정의
이동 의미론: std::move로 소유권 이전, 팩토리 반환에 활용

실무 규칙

  1. C API는 반드시 커스텀 삭제자
  2. Pimpl 소멸자는 .cpp에
  3. get()에 delete 금지
  4. 배열은 unique_ptr<T[]> 또는 vector

다음 글

shared_ptrweak_ptr, 순환 참조 해결은 C++ 스마트 포인터와 순환 참조 해결법을 참고하세요.

자주 묻는 질문 (FAQ)

Q. unique_ptr과 shared_ptr 중 뭘 써야 할까요?

A. 기본은 unique_ptr입니다. 여러 곳에서 소유권을 공유해야 할 때만 shared_ptr을 사용하세요. unique_ptr은 오버헤드가 없고, 이동만으로 명확한 소유권 전달이 가능합니다.

Q. Pimpl에서 소멸자를 왜 .cpp에 두어야 하나요?

A. unique_ptr<Impl>의 소멸자가 Impl을 delete할 때, Impl완전 타입(complete type)이어야 합니다. 헤더에서는 Impl이 전방 선언만 되어 있어 불완전 타입이므로, 소멸자를 .cpp에 두어 그 시점에 Impl이 정의된 상태로 만들어야 합니다.

Q. 배열에 unique_ptr과 vector 중 뭘 쓰나요?

A. 가변 크기, resize, push_back 등이 필요하면 vector를 쓰세요. 고정 크기이고 C API에 raw 포인터를 넘겨야 하면 unique_ptr<T[]>가 적합합니다.

Q. 선행으로 읽으면 좋은 글은?

A. C++ 스마트 포인터 기초, 이동 의미론, RAII를 먼저 읽으면 좋습니다.

참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

이 글에서 다루는 키워드 (관련 검색어)

C++, unique_ptr, 스마트포인터, 커스텀삭제자, Pimpl, 이동의미론, 메모리관리, RAII, 모던C++, C_API연동 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3