프로그래밍 언어/Effective Modern C++

[Effective Modern C++] 항목 18~22 : 스마트 포인터

우향우@ 2024. 11. 10. 18:34

항목 18 : unique_ptr

unique_ptr은 자신이 객체에 대해 유일한 소유권을 가질 때 쓰는 포인터다. 생 포인터를 받거나 make_unique함수를 통해 생성되고 소멸될 때 객체를 같이 소멸시킨다. 이동 연산만 제공하여 자신의 객체를 넘겨주고 자신은 nullptr로 초기화한다.

당연히 같은 생 포인터로 여러개를 생성하면 미정의 행동이기에 그러면 안된다. new 구문으로 바로 넘겨주거나 make_unique를 쓰는 것이 권장된다.

삭제자를 따로 지정하지 않으면 생 포인터와 동일한 비용이기에 싸다.

삭제자를 지정하면 함수 포인터의 경우 1워드, 함수 객체의 경우 그 크기만큼 메모리가 추가 소모된다.

갈무리 없는 람다의 경우 추가 메모리가 들지 않기에 유용하다.

 

항목 19 : shared_ptr

shared_ptr은 자원의 공동 소유를 가질때 쓰는 포인터다. 생 포인터나 make로 생성되면 해당 자원에 대한 제어 블록(강한 참조 회수, 약한 참조 회수, 삭제자 등을 가짐)을 생성하여 포인터를 가지고 강한 참조를 1로 둔다. 복사되면 제어 블록 참조를 복사하고 강한 참조를 1증가 시킨다. 소멸이나 reset등으로 참조를 잃으면 제어 블록의 강한 참조를 1줄이고 0이 되면 자원을 제거한다. 이동 연산 시 참조 카운트 수정없이 그냥 이동으로 넘겨준다.

shared_ptr자체의 메모리는 생 포인터의 두배, 즉 1워드 추가이다. 하지만 제어 블록의 메모리 비용과 제거시 가상함수 사용등의 비용이 든다.

마찬가지로 생 포인터로 두번 생성하면 큰일난다.

클래스에서 자신의 shared_ptr을 리턴하는 함수를 만들고 싶다면 std::enable_shared_from_this<T>를 상속하여 팩토리 함수를 구현하여 사용하면 된다.

shared_ptr은 삭제자의 경우에도 제어 블록에서 관리되기에 같은 자원을 가르키는 shared_ptr은 한 삭제자를 가지게 된다. 그리고 이로 인해 shared_ptr의 타입에는 삭제자의 타입은 포함되지 않아 유연하다.

 

항목 20: weak_ptr

weak_ptr은 shared_ptr로부터 만들 수 있으며 자원의 expired(vaild) 여부를 체크할 수 있고 shared_ptr을 생성할 수 있지만 자원 강한 참조 카운트에는 영향을 주지 않아 자신이 자원을 가르켜도 자원이 제거될 수 있다.

주로 expired 여부를 확인 후 존재하면 사용한다. lock함수를 이용하면 shared_ptr을 생성하는데 expired면 nullptr을 리턴한다. shared_ptr의 생성자에 weak_ptr을 넣으면 expitred일때 예외를 발생시킨다.

주로 자원 제거 권한을 외부에 주고 싶은 캐쉬 자료구조나, 순환 참조 방지, 관찰자 패턴등에서 사용된다.

비용은 shared_ptr과 동일하다.

 

항목 21: make_unique, shared

장점

1. 코드 치기 편함

2. new로 생성한 포인터를 스마트 포인터에 담는 구문을 함수 매개변수로 쓰면 누수 위험이 있음

3. shared의 경우 제어블록까지 한번에 할당해서 효율적임.

 

단점

1. 삭제자 지정 못함.

2. 자신만의 new, delete를 사용하는 클레스에는 사용안하는게 좋음.

3. shared_ptr의 경우 weak_ptr까지 다 사라져야 자원 해제 가능함(제어 블록과 함께 할당해서)

 

항목 22: Pimpl 관용구를  사용할 때 특수 멤버 함수들을 구현 파일에 정의하라.

Pimpl 관용구를 쓰면 헤더에서는 impl 구조체를 선언만한다. 따라서 impl 구조체에 접근해야하는 생성자등을 반드시 구조체의 정의가 있는 구현 파일에 정의해야한다.

스마트 포인터 없이 이를 구현하면 생성자에서 new impl를, 소멸자에서 delete impl를 해야한다.

unique_ptr을 사용하면 생성자에서 make_unique만 하면되고 소멸자는 필요 없어진다. 하지만 소멸자를 정의하지 않고 객체를 생성하면 컴파일 에러가 난다. 소멸자를 정의하지 않으면 헤더에 기본 소멸자가 인라인으로 생성되는데 unique_ptr<T>의 소멸자는 T에 대한 정의를 요구해서이다.그래서 빈 소멸자 정의를 구현파일에 정의해주어야 한다. 또는 구현 파일에 = default로 정의해주면 된다. 이동 및 복사도 마찬가지로 필요하든 없든 구현 파일에 정의해주자.

shared_ptr<T>의 경우 T를 삭제하는 부분이 shared_ptr 외부에 있어서 T에 대한 선언 만으로도 제거가 가능하다. 이동 및 복사도 선언만 있으면 된다. 따라서 shared_ptr에는 위 내용이 적용되지 않는다. 그냥 쓰면 된다.