반응형

 이번장에서는 c++ 11부터 제공되는 안전하고 개선된 스마트 포인터 중에 하나인  shared_ptr에 대해서 설명 드립니다. 

shared_ptr의 특징

 shared_ptr은 자원(포인터)을 참조 카운팅을 통해서 관리합니다. 

 shared_ptr는 내부적으로 자원의 주소와 참조 카운팅을 수행할 control block을 가집니다. 

 그 덕분에 하나 이상의 shared_ptr이 자원을 소유 할 수 있습니다. 자원을 소유한 shared_ptr의 객체 수가 0이 되어

 참조 카운트가 0이 될 때 소멸자를 통해서 자원을 할당 해제 합니다. 

 empty상태의 shared_ptr은 자원과 control block을 가지지 않습니다. 

 객체 할당 및 할당 해제를 위한 함수객체를 커스텀해서 전달할 수 있습니다.

 shared_ptr의 참조 카운트에 대한 수정 작업은 스레드 세이프하게 동작합니다.

 (내부적으로 vs2019에서는 WIN API인 InterLock함수를 통해서 스레드 안전성을 보장합니다. 병목이 될 수 있음!)

struct Sample {
    int content_;
    Sample(int content) : content_(content) {
        std::cout << "Constructing Sample(" << content_ << ")" << std::endl;
    }
    ~Sample() {
        std::cout << "Deleting Sample(" << content_ << ")" << std::endl;
    }
    void print(){
        std::cout << "Sample Print (" << content_ << ")" << std::endl;
    }
};

shared_ptr의 원활한 설명을 위해서 Sample이라는 구조체를 사용합니다. 

make_shared를 사용한 shared_ptr 생성

 shared_ptr을 안전하게 생성하는 것을 지원하기 위해서 make_shared 함수를 지원합니다. 

 make_unique와 마찬가지로 make_shared도 exception에 안전하고 하나의 호출로 control_block과 자원(포인터)을

 할당 하여 생성 오버헤드를 줄일 수 있습니다.

 shared_ptr을 생성할 때는 make_shared를 사용 할 것을 권장드립니다.

// make_shared 함수를 사용해서 안전하게 shared_ptr을 생성합니다. 
std::shared_ptr<Sample> pSample = std::make_shared<Sample>(1);

// 이렇게도 사용할 수 있지만 
// 위 방식보다는 조금 비 효율적입니다.
std::shared_ptr<Sample> pSample2(new Sample(1));

make_shared와 shared_ptr의 생성자 사용한 객체 생성

 위의 예제와 같이 shared_ptr을 두 가지 방법으로 생성을 했을 경우에 디버깅을 해보면 pSample과 pSample2의

 구조가 다른 것을 확인할 수 있습니다. pSample은 control block으로 Ref_count_obj2를 사용하고 

 pSample2은 control block으로 Ref_count를 사용하는 것을 확인 할 수 있습니다. 

 (내부적으로 보면 Ref_countRef_count_obj2_Ref_count_base를 상속받아서 구현되어 있습니다. )

make_shared로 shared_ptr 생성시
shared_ptr 생성자로 객체 생성시

 이것을 그림으로 그려보면 위와 같이 동작하는데 make_shared를 사용하면 한 번의 힙 메모리 할당(new Ref_count_obj2)

 으로 생성이 가능하고 연속된 메모리로 할당이 가능합니다. 

 (참조 카운트와 할당 자원은 같이 수정될 가능성이 높기 때문에 cpu 캐싱이나 메모리 페이지를 통해서 효율적으로

 접근이 될 가능성이 높습니다. )

 shared_ptr 생성자를 통한 객체 생성은 할당 자원과 Ref_count가 개별적인 힙 메모리 할당을 진행해야 하기 때문에

 make_shared로 객체를 할당 하는 것 보다 성능이 떨어집니다. 

shared_ptr의 복사 생성자 및 대입 연산자

 아래의 코드와 같이 shared_ptr객체를 복사 생성자나 복사 대입 연산자로 객체를 생성할 수 있습니다.

{
  // shared_ptr 생성 
  std::shared_ptr<Sample> pSample = std::make_shared<Sample>(1);
  {
      // 복사 생성자를 통한 shared_ptr 생성
      std::shared_ptr<Sample> pSample2(pSample);

      // 복사 대입 연산자를 통한 shared_ptr 생성
      std::shared_ptr<Sample> pSample3;
      pSample3 = pSample;
  }
}
// 최종적으로 모든 shared_ptr이 소멸되는 이시점에서 
// 할당된 자원 및 control block을 할당 해제 합니다.

복사 생성자로 생성된 shared_ptr 디버깅 비교

 예제의 코드를 수행해보면 최종적으로 control block의 _Use(참조 카운트)가 3으로 변경된 것을 확인할 수 있습니다. 

shared_ptr 복사 생성시

 그림과 같이 복사로 객체가 생성되면 shared_ptr의 자원과 control_block을 공유하는 것을 확인할 수 있습니다.

shared_ptr의 이동 생성자 및 대입 연산자

 shared_ptr은 이동 연산자를 지원합니다. 이동 연산을 하면 원본은 empty가 되고 참조 카운트가 늘어나지 않은 채

 새로운 객체를 생성합니다. 이 상태에서는 원본에 더 이상 접근하면 안 됩니다. 

    // shared_ptr의 이동 생성자를 통한 객체 생성 예제
    std::shared_ptr<Sample> pSample = std::make_shared<Sample>(1);
    std::shared_ptr<Sample> pSample2(std::move(pSample));

이동 생성자를 호출 후 상태
이동 연산자 동작후 메모리 상태

shared_ptr의 할당 해제

 참조된 모든 shared_ptr의 개수가 0이 되면 할당된 자원을 시스템에 반환합니다.

shared_ptr의 참조 카운트 내부 동작

 shared_ptr의 참조 카운트는 실제로 어떻게 수정되고 있을까요? 

 shared_ptr의 내부 구조를 간단하게 정리하면 아래 그림과 같이 구성되어 있습니다. 

shared_ptr의 참조카운트 관리 방법

 해당 구조의 클래스의 소스 코드를 추적해서 자세히 알아봅니다. 

// [shared_ptr class]
// shared_ptr의 구현 부분을 확인해보면 _Ptr_base를 상속받도록 하여 구현됩니다.
// weak_ptr도 _Ptr_base를 상속받습니다.
template <class _Ty>
class shared_ptr : public _Ptr_base<_Ty> { 
//... 내부 코드 내용들
}

 shared_ptr 클래스 구현 코드로 이동해보면 _Ptr_base 클래스를 상속받아서 구현되어 있는 것을 확인할 수 있습니다.

// [_Ptr_base class]
// @설명 : _Ptr_base 클래스는 shared_ptr과 weak_ptr의 공통된 부분을 정의하기 
//        위한 클래스입니다.
template <class _Ty>
class _Ptr_base { 
//... 내부 코드 내용들
private:
    // 자원(포인터) 정보 주소
    element_type* _Ptr{nullptr};
    // 참조 카운트 정보 주소
    _Ref_count_base* _Rep{nullptr};
}

 _Ptr_base 클래스로 이동해보면 앞에서 설명한 자원(_Ptr) 및 참조 카운트를 관리하는 변수(_Rep)를 찾을 수 있습니다.

// [_Ref_count_base class]
// @설명 : _Ref_count_base는 참조 카운트를 관리하기 위한 다양한 객체들의 공통 부분을 정의하기
//        위한 클래스입니다. 
//        make_shared에서 설명한 _Ref_count_obj2와 _Ref_count도 해당 클래스를 상속받아 구현합니다.
class __declspec(novtable) _Ref_count_base 
{ 
// common code for reference counting
private:
//... 내부 코드 내용들
    // 참조 카운트로 사용되는 변수
    _Atomic_counter_t _Uses  = 1;
    // Weak 카운트로 사용되는 변수
    _Atomic_counter_t _Weaks = 1;
public : 
    // @설명 : 참조 카운트를 증가 시킵니다.
    void _Incref() noexcept { // increment use count
        // _MT_INCR은 InterLockedIncreament로 구현되어 있습니다. 
        _MT_INCR(_Uses);
    }
    // @설명 : 참조 카운트를 감소 시킵니다.
    void _Decref() noexcept { // decrement use count
        // _MT_INCR은 InterLockedDecrement로 구현되어 있습니다. 
        if (_MT_DECR(_Uses) == 0) {
        
            _Destroy();
            _Decwref();
        }
    }
    //... 내부 코드 내용들
}

#define _MT_INCR(x) _INTRIN_RELAXED(_InterlockedIncrement)(reinterpret_cast<volatile long*>(&x))
#define _MT_DECR(x) _INTRIN_ACQ_REL(_InterlockedDecrement)(reinterpret_cast<volatile long*>(&x))

 _Ref_count_base 클래스로 이동해보면 _Uses와 _Weaks 변수를 찾을 수 있습니다.

 참조 카운트를 관리하는 변수는 _Uses입니다.

 해당 변수를 수정하는 함수를 살펴보면이전에 Window 챕터에서 설명한 InterLocked 함수를 통해서 수정되는 것을 

 확인할 수 있습니다. InterLocked 함수는 단일 객체에 대한 수정에 대해서 스레드 동기화를 지원하기 때문에 

 shared_ptr에서 참조 카운트 관련 작업은 스레드 세이프하게 동작합니다. 

 주의할 것은 실제 사용하는 자원(포인터)에 대해서는 스레드 세이프하지 않습니다. 

Shared_ptr 사용 시 주의할 점

// @설명 : 매개변수를 통해서 shared_ptr를 복사합니다.
void SampleCopyPrint(std::shared_ptr<Sample> copy)
{
    copy->print();
}

// @설명 : 매개변수를 shared_ptr를 참조합니다.
void SampleReferencePrint(std::shared_ptr<Sample>& ref)
{
    ref->print();
}

 위에 구현된 SampleCopyPrint함수와 SampleReferencePrint함수 둘 중 어떤 것이 더 좋아 보이시나요?

 참조 카운트의 동작을 이해하신 분이라면 SampleReferencePrint함수가 더욱 효율적이란 것을 이해하실 수 있습니다.

복사를 통한 참조카운트 증가는 스레드 동기화 비용증가

 참조 카운트의 증가는 스레드 동기화 비용을 수반합니다.(참조 카운트는 InterLocked 함수 사용)

 일반적으로는 비용이 크지 않지만 경합이 많이 발생되는 shared_ptr의 경우 동기화 비용이 기하급수적으로

 증가할 수 있습니다.(스레드 동기화의 고질적 문제)

 이러한 이유 때문에 shared_ptr 사용 시에는 객체의 복사가 최소한이 되도록 설계하는 것이 좋습니다. 

shared_ptr의 순환 참조 문제

 shared_ptr의 순환 참조가 발생할 수 있는데 간단히 설명하면 shared_ptr이 소유한 클래스(자원)가 멤버변수로

 같은 형태의 shared_ptr을 가지는 경우에 발생할 수 있습니다. 

 자세한 내용은 weak_ptr 글에 놓았습니다. 참조 부탁드리겠습니다.

 <링크 주소>

 

[c++] weak_ptr

이번장에서는 weak_ptr에 대해서 알아 보도록 합니다. shared_ptr를 구현하면서 참조 카운트에 영향을 받지 않는 스마트 포인터가 필요했는데 weak_ptr을 사용하면 shared_ptr가 관리하는 자원(메모리)을 참조카운..

jungwoong.tistory.com

자주 사용하는 멤버 함수

get 함수

    // 소유한 포인터를 리턴합니다. 
    _NODISCARD element_type* get() const noexcept {
        return _Ptr;
    }

 소유한 자원(포인터)에 대한 접근을 명시적으로 지원합니다. 

연산자 *, 연산자 -> 오버로딩

    template <class _Ty2 = _Ty, enable_if_t<!disjunction_v<is_array<_Ty2>, is_void<_Ty2>>, int> = 0>
    _NODISCARD _Ty2& operator*() const noexcept {
        return *get();
    }

    template <class _Ty2 = _Ty, enable_if_t<!is_array_v<_Ty2>, int> = 0>
    _NODISCARD _Ty2* operator->() const noexcept {
        return get();
    }

 shared_ptr은 연산자 *와 연산자 ->를 오버 로딩하여 해당 연산자를 호출하면 자원(포인터)에 직접 접근할 수 있습니다.

 연사자 오버 로딩 내부는 get() 함수를 통해서 구현되어 있습니다.

get 함수 및 연산자 *의 예제

     std::shared_ptr<int> pInt = std::make_shared<int>(1);
    if (pInt.use_count() > 0)
    {
        // pInt의 주소 int * 
        std::cout << "pInt.get() =" << pInt.get() << std::endl;
        
        // pInt의 주소 int 값에 직접 접근
        std::cout << "*pInt =" << *pInt << std::endl;
    }

예제 결과

reset 함수

    // reset 입력 변수가 없으면 empty shared_ptr과 swap합니다.
    // 소유하고 있는 자원의 반납합니다.
    void reset() noexcept { // release resource and convert to empty shared_ptr object
        shared_ptr().swap(*this);
    }
    // reset 입력 변수의 shared_ptr과 swap합니다.
    // 소유하고 있는 자원의 반납합니다.
    template <class _Ux>
    void reset(_Ux* _Px) { // release, take ownership of _Px
        shared_ptr(_Px).swap(*this);
    }

 reset 함수를 호출하면 자기가 소유한 자원을 반납하고 입력 변수가 있으면 해당 값으로 치환하고

 입력 변수가 없다면 empty상태로 설정합니다. 

 reset 함수 예제

    std::shared_ptr<Sample> pSample = std::make_shared<Sample>(1);
    if (pSample.use_count() > 0)
    {
        std::cout << "[NonReset] *pSample = "; 
        pSample->print();
        
        // reset으로 Sample(2) 객체 할당 -> Sample(1) 객체 할당 해제
        pSample.reset(new Sample(2));
        std::cout << "[.reset(new 2)] *pSample =";
        pSample->print();
        
        // reset으로 비어 있는 shared_ptr 셋팅 -> Sample(2) 객체 할당 해제
        pSample.reset();
        std::cout << "[.reset()] pSample.get() = " << pSample.get() << std::endl;
    }

 

예제 결과

use_count 함수

    _NODISCARD long use_count() const noexcept {
        return _Rep ? _Rep->_Use_count() : 0;
    }

 shared_ptr의 참조 카운트를 리턴합니다. 

owner_before 함수

    template <class _Ty2>
    _NODISCARD bool owner_before(const _Ptr_base<_Ty2>& _Right) const noexcept { // compare addresses of manager objects
        return _Rep < _Right._Rep;
    }

 입력된 _Rep의 주소와 자신의 _Rep의 주소를 비교합니다. 입력된 값이 크면 TRUE, 아니면 FALSE

unique 함수

    _NODISCARD _CXX17_DEPRECATE_SHARED_PTR_UNIQUE bool unique() const noexcept {
        // return true if no other shared_ptr object owns this resource
        return this->use_count() == 1;
    }

 shared_ptr의 참조 카운트가 1인지 체크해서 1이면 True, 아니면 FALSE

 

MSDN shared_ptr 주소

https://docs.microsoft.com/ko-kr/cpp/standard-library/shared-ptr-class?view=vs-2019#owner_before

 

shared_ptr class

shared_ptr class In this article --> Wraps a reference-counted smart pointer around a dynamically allocated object. Syntax template class shared_ptr; The shared_ptr class describes an object that uses reference counting to manage resources. A shared_ptr ob

docs.microsoft.com

 

반응형

'c++ > modern c++' 카테고리의 다른 글

[c++] std::move와 std::forward  (4) 2020.03.07
[c++] Move semantics  (0) 2020.03.07
[c++] 람다(Lambda) 표현식  (0) 2020.03.04
[c++] weak_ptr  (1) 2020.03.02
[c++] unique_ptr  (0) 2020.02.27

+ Recent posts