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++ 핵심 가이드라인 한글화 프로젝트 링크
- R.22 : shared_ptr를 만들때는 make_shared()를 사용하라
- R.11: 명시적인 new와 delete 호출을 지양하라
- R.1: 자원 핸들과 RAII(자원 획득시 초기화)를 사용해서 자동적으로 관리되도록 하라
- E.6: RAII를 사용해 누수를 방지하라
C++ Core Guidelines 원문 링크
- R.22 : Use make_shared() to make shared_ptrs
- R.11: Avoid calling new and delete explicitly
- R.1: Manage resources automatically using resource handles and RAII (Resource Acquisition Is Initialization)
- E.6: Use RAII to prevent leaks
'L C++' 카테고리의 다른 글
🚀성능 최적화 도구 - constexpr과 inline 함수 이해하기 (0) | 2025.03.21 |
---|---|
남들 쓰듯이 쓰는 virtual이라면 고민할 필요 없는 알쓸 virtual 정보 📖 (0) | 2024.10.06 |
직접선언! 낄낄빠빠하자. (0) | 2024.09.09 |
trivial 뜻 : 하찮은, 그렇지만 하찮지 않은 trivial‼ (0) | 2024.08.25 |
컴파일러와 페어 프로그래밍 - 특별 멤버 함수 편 🚀 (0) | 2024.08.15 |