Language/C++

C++ 11 :: Smart Pointer (Shared_ptr, Unique_ptr, Weak_ptr)

VallistA2015. 2. 14. 14:36

이미 이 전에 스마트 포인터 기능으로 auto_ptr이 존재를 하였다.

하지만, auto_ptr은 포인터의 소유권 문제가 있었다. (쓰레기 취급 받으며 안썼음)

그래서 스마트 포인터를 사용을 아에 안하거나 boost 라이브러리의 스마트 포인터를 사용했었다. (shared_ptr)


auto_ptr에서 소유권 문제가 발생하는 이유는 

복사 생성자와 할당 연산자 구현이 멤버 데이터에 대한 깊은 복사 대신 얕은 복사를 하도록 되어 있기 때문이다.

그래서 함수안으로 온전한 auto_ptr이 전달되지 않으며 auto_ptr을 전달하면 복사 생성자가 호출되고 그 결과 얕은 복사가 발생하기 때문이다. (얕은 복사와 깊은 복사의 개념을 잘 모르면 공부를 해야 한다.)

하지만 이런 auto_ptr도 문제점 투성이인 것 만은 아니고 얕은 복사를 하는 특성 덕택에 특정 순간 객체의 소유권이 유일하게 하나의 auto_ptr 객체에만 존재하게 되었다.


하지만 auto_ptr 객체 자체는 복사가 필요한 곳에 사용을 할 수 없다는 모든 장점을 다 가리는 단점이 존재해서 새로운 스마트 포인터가 대두되기 시작했다.


기존의 C++ 표준은 복사 생성자나 할당 연산자를 통해 Copy Semantics 를 지원했다. (Copy Semantics, Move Semantics를 모르는 먼저 Copy Semantics를 공부하길 .. Move Semantics는 앞으로 계속 알아갈 예정)


이는 사용자가 클래스안에 복사 생성자나 할당 연산자를 별도로 구현하지 않아도 언어 차원에서 시본으로 지원해주는 객체 복사의 개념이다. 해당 객체가 클래스 멤버로 포인터 타입의 데이터를 갖는 경우라면, 컴파일러가 만들어내는 복사 생성자와 할당 연산자에 의지하는 대신, 프로그래머가 직접 구현하여 컴파일러에 의해 생성되는 복사 생성자와 할당 연산자를 오버로딩 해야한다고 알고 있을텐데, 그 이유는 앞에서도 언급한 바와 같이 얕은 복사의 문제를 극복하려는 방법이다.


새로운 C++ 11 표준에서는 Copy Semantics에 추가로 새로운 개념인 Move Semantics가 등장하였다.

Move Semantics는 Copy Semantics 처럼 두 객체 사이에 복사를 수행하는 대신 객체의 데이터 필드 하나 하나를 이전 시키는 역할을 수행 했음. (Move Semantics를 자세히 언급안하는 이유는 언급하게되면 끝이 없기 때문에 위에도 말했다시피 천천히 알아 갈 것임)


어떤 객체를 생성해야 할 때는 일반적으로 클래스의 생성자를 호출해 만드는데, 때로는 이미 만들어진 객체의 복사는 할당을 통해서 새로운 객체를 만들기도 한다. 다른 예로 STL의 백터나 리스트와 같은 컨테이너의 경우 이들은 일종의 동적 배열이기 때문에 그 크기가 상황에 따라 두 배씩 늘어나게 된다.

이때 메모리 내부에서는 대량의 복사가 발생한다. 그런데 STL 컨테이너에서의 문제는 복사후 원본과 사본 모두를 사용하는게 아니라 원본 파괴하고 사본만 사용함.

단지 배열의 크기를 늘리기 위해 불필요한 복사 동작을 하여 오버헤드를 내게되는데, 이로인해 쓸데없는 객체를 생성하거나 사용하지 않는 원본 객체를 파괴한다는 점 등 이런 일련의 불필요한 동작은 C++ 성능저하의 주범이라는 고질적인 문제점이 있었음.


그래서 이를 위해서 복사보다는 이동이 낫겠다고 판단하여 Move Semantics를 도입하게 됨.

이를 위해 이동 생성자 라는 개념을 도입함.


그래도 문제가 남아 있었는데, Move Semantics 라는 개념을 스마트 포인터에도 적용하려니 auto_ptr의 내부 구현이 이동 시맨틱을 지원할 수 있도록 업그레이드 하기엔 기술적 제약이 걸려있고 이미 사용되던 기존 auto_ptr에 대한 호환성을 해칠수도 없었기 때문에 unique_ptr이라는 이름의 새로운 단일 포인터 타입을 구현하게 됨.

C++ 11 에서는 auto_ptr은 deprecated 됨 

unique_ptr 내부에는 복사 생성자와 할당 연산자가 아에 구현되어 있지 않는다.

따라서 우리가 기존에 알던 복사 생성자나 할당 연산자를 작성하지 않아도 컴파일러가 이를 기본적으로 제공 안해준다.

Unique_ptr은 객체가 복사가 원천 봉쇄되어 있어 이동만 가능하다.

이동도 std::move를 사용해서 이용 가능하다.


이제 소스를 보도록 하자.


Unique_ptr


1번 예제



2번 예제



3번 예제



Shared_ptr


Shared_ptr은 Unique_ptr과 틀리게 공유가 가능한 스마트 포인터다.

즉 객체 소유권을 이곳 저곳에서 관리가능하다는 것 이다.

Shared_ptr은 이곳 저곳에서 객체의 소유권을 가지고 있기 때문에 객체의 소유권이 올바르게 회수되었는지를 확인하기 위해 Reference Counting 기법을 사용한다. 

Reference Counting 기법을 사용하므로써 객체가 몇번이나 복사 되었는가 또는 새롭게 복사될 때마다. Count를 증가시키므로써 객체가 몇개 있는지를 파악할 수 있고, 객체를 해제하면 그만큼의 레퍼런스 카운트를 줄인다.


예제



Weak_ptr


Shared_ptr은 Reference Counting 기법으로 인해 실제 메모리가 몇번이나 복사되어 사용되는지 내부적으로 추적하기위해 레퍼런스 카운팅 방식을 이용했다. 하지만 이 레퍼런스 카운팅의 잠재적인 위험 가운데 하나로 서로를 참조하는 순환참조 (Circular Reference -> A는 B를 가리키고 B는 A를 가리키는 상황) 가 될 위험이 있기 때문에 이런 상황에서 순환 참조에 참여하는 모든 인스턴스가 삭제될 수 있으며, 이는 곧장 메모리 누수로 이어지는 괴랄한 상황이 발생하게 된다.

바로 이런 Shared_ptr의 문제를 해결하는 것이 Weak_ptr 이다.


Shared_ptr 에서는 메모리를 참조하는 Shared_ptr이 자신을 제외하고 하나라도 남아 있으면, 아무리 삭제 명령을 내려도 해당 메모리가 삭제되지 않는다. 해당 메모리를 가리키는 포인터 타입이 Shared_ptr이 아닌 weak_ptr이면 해당 메모리 삭제가 가능하다.

weak_ptr이 가리키는 메모리 공간은 shared_ptr이 메모리를 관리하려고 사용하는 레퍼런스 카운트에 포함되어있지 않기때문에 순환 참조가 일어날 수 없다.


예제



이제 멀티 쓰레드를 생각하는 사람이라면 동시성 문제를 생각을 하게 될 것이다.

하지만 shared_ptr과 Weak_ptr는 레퍼런스 카운팅 방식이므로 안전하게 사용할 수 있다.

단 안전하게 사용할 수 있는 경우는 각 쓰레드가 내부적으로 각각 공유 포인터가 있고 공유포인터가 동일한 리소스에 접근하는 경우이다.

하지만 각 쓰레드가 쓰레드 외부에 있는 하나의 shared_ptr 객체에 접근한다면 C++11이 제공하는 다른 원자 함수들을 사용해야한다.

댓글

댓글쓰기 폼

VallistA

병특이 끝나서 게임에서 웹으로 스위칭한 프로그래머.
프로그래밍 정보등을 공유합니다.

자고 싶습니다. ㅠㅠ

페이스북    :: 링크
카카오톡    :: kingbye1
Github      :: 링크

궁금한점 문의 주시면 답변드리도록 하겠습니다

VISITED

Today : 69

Total : 413,762

SNS

  • 페이스북아이콘
  • 카카오톡아이콘
  • 트위터아이콘