[2026] C++ make_unique & make_shared | 스마트 포인터 생성 가이드

[2026] C++ make_unique & make_shared | 스마트 포인터 생성 가이드

이 글의 핵심

std::make_unique·make_shared로 스마트 포인터를 만드는 방법, new와의 차이, make_shared의 단일 할당·캐시 지역성, 예외 안전성, make_를 피해야 하는 경우(커스텀 삭제자 등), make_unique<T[]>, 팩토리 패턴까지 정리합니다.

make_unique & make_shared란?

std::make_unique (C++14)와 std::make_shared (C++11)는 스마트 포인터를 안전하고 효율적으로 생성하는 함수입니다. new를 직접 사용하는 것보다 예외 안전하고, make_shared는 성능도 더 좋습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ new 사용
auto ptr1 = std::unique_ptr<int>(new int(10));
auto ptr2 = std::shared_ptr<int>(new int(10));
// ✅ make 함수
auto ptr1 = std::make_unique<int>(10);
auto ptr2 = std::make_shared<int>(10);

왜 필요한가?:

  • 예외 안전: 메모리 누수 방지
  • 성능: make_shared는 할당 1번 (new는 2번)
  • 간결함: 타입을 한 번만 작성
  • 명확함: 의도가 명확

make_unique와 new의 차이

std::make_unique<T>(args...)는 구현상 new T(...)와 동등한 생성을 한 번에 감싼 것이지만, 호출부에서는 raw 포인터가 드러나지 않습니다.

관점new + 스마트 포인터 생성자make_unique / make_shared
표현T를 여러 번 쓰거나 new 결과가 중간에 남음타입은 한 번, 곧바로 스마트 포인터
예외 안전 (구 규칙)여러 전체 표현식이 섞이면 순서 이슈단일 함수 호출로 생성 경로가 단순
커스텀 삭제자생성자에 직접 넘기기 쉬움표준 make_unique / make_shared삭제자 인자 없음
private 생성자같은 클래스의 new는 가능make_shared는 비프렌드 외부에서 호출 불가인 경우가 있음
즉, “기본 delete로 충분한 동적 객체”에는 make_가 기본값이고, delete가 아닌 정리(파일 닫기, delete[], free 등)나 접근 제어가 막힌 생성에서는 new(또는 allocate_shared 등) 경로가 남습니다.

make_shared의 메모리 최적화 (심화)

std::shared_ptr관리되는 객체제어 블록(강한 참조·약한 참조 카운트, 커스텀 삭제자/할당자 정보 등)을 둘 다 추적해야 합니다. shared_ptr<T>(new T) 형태는 흔히 객체용 메모리제어 블록용 메모리각각 할당합니다. 반면 make_shared<T>(args...)는 구현에 따라 한 번의 연속된 할당에 객체와 제어 블록을 함께 둘 수 있어, 다음이 기대됩니다.

  • 할당 횟수 감소: 힙 트래킹·락 경합이 있는 환경에서 체감될 수 있음.
  • 지역성: 객체와 제어 블록이 인접하면 캐시 친화적일 수 있음.
  • 오버헤드: 두 번의 operator new 호출을 한 번으로 줄이는 효과. 다만 객체가 매우 크고 weak_ptr이 오래 살아 남는 경우, make_shared로 묶인 덩어리 때문에 객체 본문이 쓸모없어진 뒤에도 메모리가 통째로 유지되는 현상이 생길 수 있습니다(아래 “메모리 해제 타이밍”과 FAQ 참고). 그런 프로파일이면 shared_ptr<T>(new T)가 유리할 수 있습니다. 일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

예외 안전성 심화

위의 func(std::unique_ptr<int>(new int(10)), compute()) 예는 C++17 이전에는 인자 평가 순서가 제한적으로만 보장되어, new까지 실행된 뒤 compute()에서 예외가 나면 unique_ptr이 만들어지지 않아 누수가 날 수 있었습니다. make_unique 한 번의 호출로 객체 생성과 스마트 포인터 포장이 한 경로로 묶이면 이런 “중간에 raw 소유권이 남는 창”이 줄어듭니다. C++17부터는 함수 호출의 인자 표현식에 대해 더 엄격한 순서 규칙이 생겼지만, 여전히 make_가 의도를 분명히 하고 실수 여지를 줄인다는 점에서 권장 패턴으로 남습니다. 강한 예외 안전을 요구하는 코드에서는 “raw new 결과를 지역 변수에 담은 뒤 unique_ptr로 이전”처럼 단계를 나누는 것도 한 방법입니다.

언제 make_unique / make_shared를 쓰지 말아야 하나

다음은 대표적으로 new(또는 allocate_shared) + 생성자 쪽이 맞는 경우입니다.

  1. 커스텀 삭제자가 필요할 때 (delete가 아닌 정리). make_unique는 삭제자를 받지 않습니다.
  2. std::allocate_shared커스텀 할당자로 제어 블록·객체 배치를 정밀하게 잡아야 할 때.
  3. shared_ptr의 생성이 클래스 내부 전용이어야 할 때(enable_shared_from_this와 함께 쓰는 패턴 등) — 설계에 따라 make_shared를 밖에서 부르지 않습니다.
  4. make_shared로 인한 메모리 지연 해제가 문제일 때(큰 객체 + 오래 사는 weak_ptr).
  5. 배열: make_unique<T[]>(n)는 가능하지만, shared_ptr의 배열은 C++20 std::make_shared<T[]>(n) 이전에는 관례적으로 커스텀 삭제자나 shared_ptr 특수화를 썼습니다. 자세한 커스텀 삭제자 내용은 C++ Custom Deleters 가이드를 참고하세요.

배열: make_unique<T[]>(n) 심화

C++14부터 std::make_unique<T[]>(n)길이 n의 동적 배열을 만들고, 삭제는 delete[]에 맞춰집니다. 주의할 점은 다음과 같습니다.

  • 요소별 생성자 호출이 필요하면 std::vector<T>가 더 단순한 경우가 많습니다.
  • make_unique<int[]>(10)기본 초기화 규칙을 따릅니다(타입에 따라 초기화되지 않은 값이 있을 수 있음). 값을 0으로 두려면 std::vector나 루프 초기화를 고려하세요.
  • C++20에서는 std::make_shared<T[]>(n)shared_ptr 배열 생성이 표준화되었습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 동적 배열 — delete[]와 짝을 맞춤
auto p = std::make_unique<std::string[]>(4);
p[0] = "a";
// C++20: shared 배열
auto s = std::make_shared<double[]>(100);

실전 팩토리 패턴 (보강)

팩토리는 구체 타입을 숨기고 인터페이스만 노출할 때 unique_ptr/shared_ptr와 잘 맞습니다.

  • 추상 베이스 + unique_ptr<Base> 반환: 소유권을 호출자에게 넘기며, 구현 파일에서만 파생 클래스를 new하거나 make_unique합니다.
  • 실패 가능한 생성: std::optional<std::unique_ptr<T>> 또는 expected 스타일로 “생성 실패”를 명시적으로 표현합니다.
  • 공유 캐시: 동일 키에 대해 shared_ptr을 재사용하려면 make_shared로 한 번 만들고 맵에 넣는 패턴이 흔합니다(아래 “공유 캐시” 예시 참고). 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
struct Shape { virtual ~Shape() = default; };
struct Circle : Shape {};
inline std::unique_ptr<Shape> make_shape(const std::string& kind) {
    if (kind == "circle") {
        return std::make_unique<Circle>();
    }
    return nullptr; // 또는 optional
}

Database::connect 예시처럼 private 생성자 + 정적 멤버에서만 make_unique<Impl>을 호출하면, 호출자는 구체 DB 타입을 몰라도 됩니다.

장점 상세

1. 예외 안전성

다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void func(std::unique_ptr<int> ptr, int value) {
    // ...
}
int compute() {
    throw std::runtime_error("에러");
}
// ❌ new: 예외 시 누수 가능
func(std::unique_ptr<int>(new int(10)), compute());
// 실행 순서가 보장되지 않음:
// 1. new int(10)
// 2. compute() - 예외 발생!
// 3. unique_ptr 생성 (실행 안됨)
// → 메모리 누수
// ✅ make_unique: 안전
func(std::make_unique<int>(10), compute());
// make_unique는 원자적으로 실행
// → 메모리 누수 없음

이유: C++17 이전에는 함수 인자의 평가 순서가 정의되지 않았습니다. newunique_ptr 생성 사이에 예외가 발생하면 메모리 누수가 발생할 수 있습니다.

2. 성능 (shared_ptr)

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

// ❌ new: 할당 2번
auto ptr1 = std::shared_ptr<int>(new int(10));
// 1. int 할당 (4바이트)
// 2. 제어 블록 할당 (참조 카운트 등)
// ✅ make_shared: 할당 1번
auto ptr2 = std::make_shared<int>(10);
// int + 제어 블록 함께 할당

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

// new 사용:
// [int] (힙 영역 1)
// [제어 블록] (힙 영역 2)
// make_shared:
// [int | 제어 블록] (힙 영역 1)

성능 비교:

  • 할당 횟수: make_shared 1번 vs new 2번
  • 캐시 지역성: make_shared가 더 좋음 (연속된 메모리)
  • 할당 오버헤드: make_shared가 더 적음

3. 간결함

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

// ❌ new: 타입 2번
auto ptr = std::unique_ptr<VeryLongTypeName>(new VeryLongTypeName(args));
// ✅ make_unique: 타입 1번
auto ptr = std::make_unique<VeryLongTypeName>(args);

실전 예시

예시 1: 기본 사용

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

// unique_ptr
auto ptr1 = std::make_unique<int>(42);
auto ptr2 = std::make_unique<std::string>("Hello");
auto ptr3 = std::make_unique<std::vector<int>>(10, 0);
// shared_ptr
auto ptr4 = std::make_shared<int>(42);
auto ptr5 = std::make_shared<std::string>("Hello");

예시 2: 배열

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++14: make_unique 배열
auto arr1 = std::make_unique<int[]>(10);
// C++20: make_shared 배열
auto arr2 = std::make_shared<int[]>(10);
// 초기화
for (int i = 0; i < 10; i++) {
    arr1[i] = i;
}

예시 3: 예외 안전성

다음은 cpp를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

void process(std::unique_ptr<Widget> w, int value) {
    // ...
}
int compute() {
    throw std::runtime_error("에러");
}
int main() {
    // ❌ 예외 시 누수 가능
    // process(std::unique_ptr<Widget>(new Widget()), compute());
    
    // ✅ 안전
    process(std::make_unique<Widget>(), compute());
}

예시 4: 팩토리

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

class Widget {
public:
    static std::unique_ptr<Widget> create(int id) {
        return std::make_unique<Widget>(id);
    }
    
private:
    Widget(int id) {}  // private 생성자
};

make_shared 성능

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

// ❌ 할당 2번
auto ptr = std::shared_ptr<int>(new int(10));
// 1. int 할당
// 2. 제어 블록 할당
// ✅ 할당 1번
auto ptr = std::make_shared<int>(10);
// int + 제어 블록 함께 할당

자주 발생하는 문제

문제 1: 커스텀 삭제자

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

// ❌ make_unique는 커스텀 삭제자 불가
auto deleter = [](int* p) { delete p; };
// auto ptr = std::make_unique<int, decltype(deleter)>(10, deleter);
// ✅ 생성자 사용
auto ptr = std::unique_ptr<int, decltype(deleter)>(
    new int(10), deleter
);

문제 2: 초기화 리스트

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

// ❌ 중괄호 초기화
// auto ptr = std::make_unique<std::vector<int>>({1, 2, 3});
// ✅ 소괄호
auto ptr = std::make_unique<std::vector<int>>(
    std::initializer_list<int>{1, 2, 3}
);

문제 3: private 생성자

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

class Widget {
    Widget() {}  // private
    
public:
    static std::shared_ptr<Widget> create() {
        // ❌ make_shared 불가
        // return std::make_shared<Widget>();
        
        // ✅ new 사용
        return std::shared_ptr<Widget>(new Widget());
    }
};

문제 4: 메모리 해제 타이밍

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

// make_shared: 제어 블록과 객체 함께 할당
auto ptr = std::make_shared<LargeObject>();
std::weak_ptr<LargeObject> weak = ptr;
ptr.reset();  // 객체 소멸하지만 메모리는 weak_ptr 때문에 유지
// ✅ new 사용 시 객체 메모리만 해제
auto ptr = std::shared_ptr<LargeObject>(new LargeObject());

권장사항

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

// ✅ 기본적으로 make 함수
auto ptr1 = std::make_unique<Widget>();
auto ptr2 = std::make_shared<Widget>();
// ❌ new 사용 (특별한 경우만)
// - 커스텀 삭제자
// - private 생성자
// - 메모리 해제 타이밍 제어

실무 패턴

패턴 1: 팩토리 메서드

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

class Database {
public:
    static std::unique_ptr<Database> connect(const std::string& url) {
        auto db = std::make_unique<Database>();
        db->connect_impl(url);
        return db;
    }
    
private:
    Database() = default;
    void connect_impl(const std::string& url) {
        // 연결 로직
    }
};
// 사용
auto db = Database::connect("postgres://localhost");

패턴 2: 리소스 관리

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

class FileHandle {
    FILE* file_;
    
public:
    static std::unique_ptr<FileHandle> open(const std::string& path) {
        auto handle = std::make_unique<FileHandle>();
        handle->file_ = fopen(path.c_str(), "r");
        if (!handle->file_) {
            throw std::runtime_error("파일 열기 실패");
        }
        return handle;
    }
    
    ~FileHandle() {
        if (file_) {
            fclose(file_);
        }
    }
    
private:
    FileHandle() : file_(nullptr) {}
};
// 사용
auto file = FileHandle::open("data.txt");
// 자동으로 파일 닫힘

패턴 3: 공유 캐시

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

class Cache {
    std::map<std::string, std::shared_ptr<Data>> cache_;
    
public:
    std::shared_ptr<Data> get(const std::string& key) {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            return it->second;  // 공유
        }
        
        // 새로 생성
        auto data = std::make_shared<Data>(key);
        cache_[key] = data;
        return data;
    }
};
// 사용
Cache cache;
auto data1 = cache.get("user:123");
auto data2 = cache.get("user:123");  // 같은 객체 공유

FAQ

Q1: make 함수의 장점은?

A:

  • 예외 안전성: 메모리 누수 방지
  • 성능: make_shared는 할당 1번
  • 간결함: 타입을 한 번만 작성
  • 명확함: 의도가 명확 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// make_unique: 예외 안전
func(std::make_unique<int>(10), compute());
// make_shared: 성능
auto ptr = std::make_shared<int>(10);  // 할당 1번

Q2: 언제 new를 사용하나요?

A:

  • 커스텀 삭제자: make_unique는 커스텀 삭제자 불가
  • private 생성자: make_shared는 private 생성자 접근 불가
  • 메모리 해제 타이밍: make_sharedweak_ptr 때문에 메모리 해제 지연 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 커스텀 삭제자
auto deleter = [](FILE* f) { if (f) std::fclose(f); };
auto ptr = std::unique_ptr<FILE, decltype(deleter)>(
    std::fopen("file.txt", "r"), deleter
);

Q3: 배열은 어떻게 생성하나요?

A:

  • make_unique: C++14부터 배열 지원
  • make_shared: C++20부터 배열 지원 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// make_unique: C++14
auto arr1 = std::make_unique<int[]>(10);
arr1[0] = 42;
// make_shared: C++20
auto arr2 = std::make_shared<int[]>(10);
arr2[0] = 42;

Q4: make_shared의 성능 차이는?

A: 할당 1번 vs 2번입니다. make_shared는 객체와 제어 블록을 함께 할당합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// new: 할당 2번
auto ptr1 = std::shared_ptr<int>(new int(10));
// 1. int 할당
// 2. 제어 블록 할당
// make_shared: 할당 1번
auto ptr2 = std::make_shared<int>(10);
// int + 제어 블록 함께 할당

벤치마크:

  • make_shared: ~50ns
  • new + shared_ptr: ~100ns

Q5: 초기화 리스트는 어떻게 사용하나요?

A: 소괄호 사용이 필요합니다. 중괄호는 직접 사용할 수 없습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 중괄호: 직접 사용 불가
// auto ptr = std::make_unique<std::vector<int>>({1, 2, 3});
// ✅ 소괄호 + initializer_list
auto ptr = std::make_unique<std::vector<int>>(
    std::initializer_list<int>{1, 2, 3}
);
// ✅ 또는 임시 벡터
auto ptr2 = std::make_unique<std::vector<int>>(
    std::vector<int>{1, 2, 3}
);

Q6: make_shared의 단점은?

A: 메모리 해제 지연입니다. weak_ptr이 남아있으면 객체는 소멸되지만 메모리는 해제되지 않습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

auto ptr = std::make_shared<LargeObject>(1000000);
std::weak_ptr<LargeObject> weak = ptr;
ptr.reset();  // 객체 소멸, 하지만 메모리는 유지 (weak_ptr 때문)
// new 사용 시:
auto ptr2 = std::shared_ptr<LargeObject>(new LargeObject(1000000));
std::weak_ptr<LargeObject> weak2 = ptr2;
ptr2.reset();  // 객체 메모리 즉시 해제, 제어 블록만 유지

Q7: make_unique는 C++11에 없나요?

A: 없습니다. C++14에 추가되었습니다. C++11에서는 직접 구현할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// C++11: 직접 구현
template<typename T, typename....Args>
std::unique_ptr<T> make_unique(Args&&....args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Q8: make 함수 학습 리소스는?

A:


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

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

관련 글

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