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

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

이 글의 핵심

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을 조합한 팩토리·컨테이너 패턴을 적용할 수 있습니다.
  • 자주 하는 실수와 프로덕션 체크리스트를 활용할 수 있습니다.

목차

  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다른 해제 함수가 필요한 리소스는 커스텀 삭제자를 지정해야 합니다.

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

#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())를 호출합니다.

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

#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* 래퍼 (파일 핸들)

#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 (예시)

#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[]를 사용합니다.

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

배열 생성과 접근

#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 연동: 고정 크기 버퍼

#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에서는 초기화가 보장되지 않을 수 있으므로, 명시적으로 초기화하려면:

// 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 안정성과 빌드 시간 단축을 얻을 수 있습니다.

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 구현

// 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_;
};
// 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이 전방 선언만 되어 있어 불완전 타입이므로 컴파일 에러가 발생합니다.

// ❌ 잘못된 예: 헤더에 소멸자 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은 복사가 삭제되어 있고, 이동만 가능합니다. 이는 “독점 소유권”을 타입 시스템으로 강제하는 설계입니다.

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

이동 시맨틱스 기본

#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;
}

함수 인자: 소유권 이전

#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와 이동

#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 저장

#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 스타일)

#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 + 팩토리

// 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);
};
// 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: 배열 + 커스텀 삭제자 (알려진 크기)

#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 이동

#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'

// ❌ 잘못된 코드 (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 역참조 또는 정의되지 않은 동작.

// ❌ 잘못된 코드
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을 복사로 함수에 전달

증상: 컴파일 에러.

// ❌ 잘못된 코드
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[]>를 사용합니다.

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

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

선택 플로우차트

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 (배열)

// 고정 크기, 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 포인터 (소유권 표현)

// ✅ 소유권 있음: unique_ptr
class Owner {
    std::unique_ptr<Resource> resource_;
};

// ✅ 소유권 없음 (참조만): raw 포인터 또는 참조
void process(const Resource* r);
void process(const Resource& r);

9. 프로덕션 패턴

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

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 래퍼 클래스

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)

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)

class Parser {
    std::unique_ptr<Cache> cache_;  // 필요 시에만 생성

public:
    void enableCache() {
        cache_ = std::make_unique<Cache>();
    }
    void parse() {
        if (cache_) {
            cache_->lookup(/* ... */);
        }
    }
};

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

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

// ✅ 권장: 예외 안전, 한 줄
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·shared_ptr
  • C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리
  • C++ Move Semantics | std::move로 불필요한 복사 제거하고 성능 최적화
  • C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]

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

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


관련 글

  • C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
  • C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
  • C++ Google Mock |
  • C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
  • C++ 이동 의미론 완벽 가이드 | rvalue 참조·std::move
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3