일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 영화
- 티스토리챌린지
- virtual function
- 보편 참조
- resource management class
- 예외
- 게임
- exception
- Effective c++
- std::async
- effective modern c++
- c++
- lua
- 언리얼
- 오블완
- reference
- implicit conversion
- 반복자
- operator new
- iterator
- 상속
- Smart Pointer
- universal reference
- 스마트 포인터
- 참조자
- 영화 리뷰
- effective stl
- UE4
- more effective c++
- 암시적 변환
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 19. std::shared_ptr 본문
728x90
항목 19. 소유권 공유 자원의 관리에는 std::shared_ptr를 사용하라
C++11에서의 std::shared_ptr
- 가비지 컬랙팅의 편리함과 수동 수명 관리의 소멸 시점 예측성을 모두 갖춘 자원 관리 시스템
- 공유 소유권(shared ownership) 의미론으로 관리한다.
- 참조 카운트(reference count)를 사용하여 자원을 관리한다.
참조 카운트 관리와 성능
- std::shared_ptr의 크기는 raw 포인터의 두배이다.
- 참조 카운트 포인터도 저장해야 하기 때문이다.
- 참조 카운트를 담을 메모리를 반드시 동적으로 할당해야 한다.
- 공유 포인터가 가리키는 객체와는 별개로 할당되어야 하기 때문이다.
- make_shared를 사용하면 동적 할당 비용을 피할 수 있다.
- 참조 횟수의 증가와 감소가 반드시 아토믹(atomic)해야 한다.
- 여러 쓰레드가 참조 카운트를 동시에 읽고 쓰려고 할 수 있기 때문이다.
- 아토믹 연산은 일반적인 연산보다 느리다.
레퍼런트 카운트를 올리지 않는 경우
- 이동 생성을 할 때는 카운트를 올리고 낮추는 것이 쓸데없는 자원의 소모일 뿐이다.
std::unique_ptr과의 차이
- 커스텀 삭제자가 타입에 반영되지 않는다.
auto loggingDel = [](Widget* pw)
{
makeLogEntry(pw);
};
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
std::shared_ptr<Widget> spw(new Widget, loggingDel);
// 덕분에 공유 포인터가 더 유연하게 사용할 수 있다.
// 서로 다른 커스텀 삭제자들
auto customDeleter1 = [](Widget* pw) { ... };
auto customDeleter2 = [](Widget* pw) { ... };
// 아래 두 공유 포인터는 동일한 타입이므로
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
// 같은 컨테이너에 넣을 수 있다.
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
- 커스텀 삭제자를 지정해도 std::shared_ptr 객체의 크기가 변하지 않는다.
- 삭제자를 할당한 메모리가 std::shared_ptr 객체의 일부가 아니기 때문이다.
- 참조 카운트가 포함된 포인터 쪽의 공간에서 관리한다.
- 공유 포인터 자체는 참조 카운트를 포함하는 제어 블록(control block)의 포인터만 가지므로 당연히 객체의 크기에는 변화가 없다.
제어 블록이 생성되는 타이밍
- std::make_shared(항목 21 참조)를 사용하여 std::shared_ptr 객체를 생성하는 경우
- 이 함수는 공유 포인터가 가리킬 객체를 새로 생성하기 때문에, 이전에 제어 블록이 존재할 리가 없다.
- 고유 소유권 포인터(std::unique_ptr 혹은 std::auto_ptr)로부터 std::shared_ptr 객체를 생성하는 경우
- raw 포인터로 std::shared_ptr 생성자를 호출하는 경우
하나의 raw 포인터로 여러 std::shared_ptr을 생성하면 미정의 행동
auto pw = new Widget;
...
std::shared_ptr<Widget> spw1(pw, loggingDel); // 공유 포인터 생성
...
std::shared_ptr<Widget> spw2(pw, loggingDel); // 또 다른 공유 포인터 생성?
// 제어 블록이 두 개가 된다!
// 범위를 벗어나면 파괴가 두 번 시도되게 된다!!(미정의 행동!)
- 위의 예는 동적 할당 객체의 raw 포인터를 만드는 것이 왜 나쁜지의 또 다른 반례이기도 하다.
- 잘못된 용법에서 배울 점
- std::shared_ptr 생성자에 raw 포인터를 넘기는 일은 피하자.
- std::make_shared를 사용한다.
- 헌데 이걸 사용할 경우 커스텀 삭제자를 사용하지 못한다는 문제가 있긴 하다.
- std::shared_ptr 생성자를 raw 포인터로 호출할 수밖에 없는 상황이라면, new의 결과를 직접 전달하자.
-
std::shared_ptr<Widget> spw1(new Widget, loggingDel); // new를 직접 사용
-
- std::shared_ptr 생성자에 raw 포인터를 넘기는 일은 피하자.
- 또 다른 잘못된 용법 예
std::vector<std::shared_ptr<Widget>> processedWidgets;
class Widget
{
public:
...
void process();
...
};
void Widget::process()
{
...
processedWidgets.emplace_back(this); // 잘못된 방식!
// 밖에서 이미 공유 포인터로 관리되고 있다면 큰일이다!
}
- 위의 경우 std::enable_shared_from_this 템플릿을 사용하여 해결한다.
class Widget : public std::enable_shared_from_this<Widget>
{
// shared_from_this() 멤버 함수가 추가된다.
// 현재 객체에 대한 제어 블록을 조회하고,
// 그 제어 블록을 가리키는 공유 포인터를 생성한다.
// 즉, 밖에서 std::shared_ptr가 반드시 존재한다는 가정이 있다.
public:
// 그러므로 팩토리 함수도 하나 제공해준다.
template<typename... Ts>
static std::shared_ptr<Widget> create(Ts&&... params);
...
void process();
...
private:
... // 생성자들
// std::shared_ptr가 하나도 없는 상황에서 shared_from_this가 불리지 않도록
// 생성자는 private 처리한다.
};
void Widget::process()
{
...
// 유효하다.
processedWidgets.emplace_back(shared_from_this());
}
std::shared_ptr에 대한 추가적인 이야기
- 제어 블록의 자원 소모
- 크기는 몇 워드 정도이지만, 커스텀 삭제자나 할당자 때문에 더 커질 수도 있다.
- 구현이 생각보다 복잡하다.
- 상속을 활용하며, 가상함수도 있다.
- 그래봤자 공유 포인터의 이점에 비하면 터무니없이 작은 자원 소모.
- 혹 독점 소유권으로 충분하다면 std::unique_ptr을 사용하면 된다.
- 언제든 shared_ptr로 업그레이드 할 수 있다.
- 하지만 std::shared_ptr은 unique_ptr로 바꾸기 쉽지 않다.
- 배열 관리를 할 수 없다.
- 즉, std::shared_ptr<T[]> 같은 것이 없다.
- std::shared_ptr<T>로 배열을 가리키되, 커스텀 삭제자에서 적법한 처리를 해주면 어떨까?
- 허나 std::shared_ptr는 operator[]를 제공하지 않으므로 어색한 표현식을 동원해야 한다.
- 그리고 std::shared_ptr는 파생 클래스 포인터에서 기반 클래스 포인터로의 변환을 지원하는데,
- 배열에서는 그게 합당하지가 않다.
- 또한! C++11은 그런 불편한 방식 외의 다양한 대안들이 있다.
- std::array, std::vector, std::string 등
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 21. std::make_unique, std::make_shared (0) | 2025.03.06 |
---|---|
[Effective Modern C++] 20. std::weak_ptr (0) | 2025.03.05 |
[Effective Modern C++] 18. std::unique_ptr (0) | 2025.02.28 |
[Effective Modern C++] 17. 특수 멤버 함수(special member function) (1) | 2025.02.27 |
[Effective Modern C++] 16. thread safety한 const 멤버 함수 (0) | 2025.02.26 |
Comments