SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++

🤔 스마트 포인터(unique_ptr, shared_ptr) 생성 시 make_shared(or make_unique)를 써야 하는 이유?

보리남편 김 주부 2025. 3. 14. 01:21

Q1. 스마트 포인터 unique_ptr, shared_ptr를 모른다.
Q2. 스마트 포인터 unique_ptr, shared_ptr 생성 방법은?
Q3. make_shared(or make_unique)를 써야 하는 이유는?

* 3가지 질문에 대한 답을 알고 있다면 이 글을 읽지 않아도 된다. 😎🛫


shared_ptr 생성 시에는 std::make_shared를 써라!


C++에서 메모리 할당/해제는 의도치 않은 많은 이슈를 양산하기에 RAII(Resource acquisition is initialization)가 되는 걸 사용하라고 한다. 그중 하나인 shared_ptr에 대해 조금 알아보자.

 

sample
#include <iostream>
#include <memory>

struct Point
{
    int x;
    int y;

    Point(int a, int b) : x(a), y(b){}
};

int main() { 
    Point* p1 = new Point(1,2);

    //1.
    //std::shared_ptr<Point> sp1 = new Point(1,2); // error
    
    //2.
    std::shared_ptr<Point> sp2(new Point(1,2)); // possible

    //3.
    std::shared_ptr<Point> sp3 = std::make_shared<Point>(1,2);
    return 0;
}

 

Point* p1 = new Point(1,2); // 이 코드를 shared_ptr로 만들 때,

1. std::shared_ptr<Point> sp1 = new Point(1,2); //error
이렇게는 에러가 발생한다. 
2. std::shared_ptr<Point> sp2(new Point(1, 2));
shared_ptr는 원시 포인터를 취급하지 않는 듯 보이지만 이와 같은 방식은 허용하긴 한다.
🗨️ 굳이 해석을 해보자면 'shared_ptr은 포인터와 명백히 다른 타입이다. '하지만 네가 포인터를 넣어서 생성해 주는 건 허용해 줄게'라는 의도인가?🤔

 

아무튼 1. 은 에러이고, 2. 는 동작되는 이유를 알아보자.

1. 은 이동 생성자를 포인터(point *)로 받는 걸 정의해 놓지 않아 발생한 에러였고,



2. 는 explicit로 포인터만 받을 수 있게 설계되어 있어 가능은 하다.

 

가능은 하지만 make_shared를 사용하기 위해

[원시포인터 생성] VS [make_shared] 내부 동작 비교

struct Point
{
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
};

1. std::shared_ptr sp1(new Point(1, 2));         //possible

2. std::shared_ptr sp2 = std::make_shared(1, 2); // best

 

1. 의 내부 동작은 아래와 같은 순서로 동작을 한다.

 

1) operator new : 생성자 없이 메모리 할당
2) new (할당된 주소) Point(1,2) : 객체 생성자 호출
3) shared_ptr 만들어서 반환

 

🗨️참고. 1. 의 내부동작이 잘 이해가 안 된다면 참고 https://jabdon4ny.tistory.com/128
 

new/delete 동작 원리 이해하기

💬 C++ core guidelines에 따르면 메모리 관리는 RAII가 되는 smart pointer를 권장하고 있다.하지만 이미 레거시 코드에 녹여져 있는 new/delete는 어떻게 해야 하나?🤔 또 malloc/free는?😨C++에서 malloc/free를

jabdon4ny.tistory.com

 

만일 아래 코드가 (B) -> (C) -> (A) 순서로 호출될 때 (C)에서 예외가 발생하면 메모리 leak이 발생되지만,

//(A)      (B)                (C)
foo(std::shared_ptr(new int), goo());

 

동일한 예외 상황에서 아래 코드(make_shared)는

//(A)      (B)                (C)
foo(std::make_shared<int>(0), goo());

 

 자원 획득과 핸들(주소)을 한 번에 수행하기에(하나의 작업)!! 메모리 leak은 발생하지 않는다.

 

 

그래서 shared_ptr를 생성할 때는 make_shared로 생성하라고 하는 것이다.

 

📜C++ core guidelines
R.22 : Use make_shared() to make shared_ptrs
R.22 : shared_ptr를 만들 때는 make_shared()를 사용하라

 

원시포인터를 shared ptr로 전환할 때 주의해야 하는 내용을 생각해 보자.


🗨️ 원시 포인터를 받는 이유는 기존 레거시 코드를 대체하기 위한 작업이 아닐까?

 

shared ptr에 원시 포인터를 전달하는 경우는 두 가지를 생각해 볼 수 있다.

1. std::shared_ptr sp2(new int);

2. int* p = new int;
   std::shared_ptr sp1(p);

 

1. 의 경우는 그냥 shared_ptr를 생성하기 위한 원시 포인터이고,

2. 의 경우는 코드가 연속적으로 작성이 되었지만, 어딘가 모를 기존에 생성된 원시포인터를 shared_ptr로 변경하는 레거시 코드이다.

 

1. 의 경우는 make_shared로 대체를 하고, 2. 의 경우 주의하지 않으면 문제가 발생할 수 있는데 아래코드의 문제점을 생각해 보자.

int main(){
    int* p = new int;
    std::shared_ptr sp1(p);
    std::shared_ptr sp3(p);
    return 0;
}

 


 

결론을 먼저 얘기하면 이중 free를 했다고 예외가 발생한다.

 

한 줄 한 줄 해석을 해보면

int* p = new int;
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp3(p);

 

하나의 원시 포인터로 두 개의 shared ptr을 생성하다 보니

sp1에서 제어블록생성과 레퍼런스 카운터가 '1'이 되었다.

sp3에서 새로운 제어블록이 생성되어, 레퍼런스 카운터가 '1'이 되었다.

두 제어블록은 서로 모르지만 동일한 메모리 주소를 가지는 아래 그림과 같이 된다. 

 

main 함수 종료 시, sp3가 소멸될 때 레퍼런스 카운터가 '0'이 되면서 delete을 수행했는데, sp1도 레퍼런스 카운터가 '0'이 되어 이미 해제된 메모리를 다시 delete 하려다 예외가 발생한 것이다.

 

문제의 코드는 아래와 같이 수정할 수 있고,

int main() {
    int* p = new int;
    std::shared_ptr sp1(p);
    std::shared_ptr sp3 = sp1;
    return 0;
}

 

이 코드는 하나의 제어블록으로 공유한다. 

원시 포인터 p를 sp1으로 만들면서 제어블록생성과 레퍼런스 카운터가 1이 되었는데, sp3는 sp1에 복사생성이 되면서 sp1의 제어블록을 공유하고 래퍼런스 카운터가 '2'가 된다. (아래 그림 참조)

 

 

이 형태가 왜 안전하냐면, main 함수 종료 시,  sp3가 소멸될 때 레퍼런스 카운터가 2->1이 되고 sp1이 소멸될 때 레퍼런스가 '0'이 되면서 delete로 안전하게 메모리가 해제되기 때문이다.

 

🗨️ 이처럼 스마트 포인터를 사용하려다 오히려 예외가 발생한다면 '그럼 그렇지 무슨 RAII야'라고 하며 익숙한 원시 포인터를 사용할 텐데 이런 주의사항을 잘 이해하고 현명하게 사용하기 바란다.

 

참고


C++ 핵심 가이드라인 한글화 프로젝트 링크

C++ Core Guidelines 원문 링크

728x90