[Modern C++] 항목 17: 특수 멤버 함수들의 자동 작성 조건을 숙지하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 4. 똑똑한 포인터
🧠 생 포인터의 문제점
생 포인터(raw pointer)를 사랑하기 힘든 이유들:
- 선언만 봐서는 하나의 객체를 가리키는지 배열을 가리키는지 구분할 수 없다.
- 선언만 봐서는 다 사용한 후 포인터가 가리키는 객체를 독자가 직접 파괴해야 하는지, 즉 포인터가 가리키는 객체를 소유하고 있는지 여부를 알 수 없다.
- 가리키는 객체를 직접 파괴해야 한다는 점을 알게 된다고 해도,
delete를 사용해야 할지, 다른 파괴 메커니즘을 사용해야 할지 알 수 없다. delete를 이용해서 파괴해야 함을 알게 된다고 해도, 단일 객체 삭제(delete)를 사용해야 할지, 배열 버전(delete [])을 사용해야 할지 알 수 없는 경우가 있다.- 포인터가 가리키는 객체를 소유하고 있으며 파괴하는 구체적인 방법을 알아냈다고 해도, 코드의 모든 경로에서 파괴가 정확히 한 번 일어남을 보장하기가 어렵다.
- 포인터가 가리키는 객체를 잃었는지를 알아내는 방법은 없다. 포인터가 가리키는 메모리 장소에 유효한 객체가 더 이상 존재하지 않는 상황을 파악할 수 없다.
해결책: 똑똑한 포인터(smart pointer)
- 똑똑한 포인터는 생 포인터를 감싸는 일종의 래퍼(wrapper)로, 생 포인터와 아주 비슷한 방식으로 작동하되 그 함정들을 피할 수 있게 한다.
- C++11의 똑똑한 포인터는 총 네 가지:
std::auto_ptr,std::unique_ptr,std::shared_ptr,std::weak_ptr std::auto_ptr는 C++98에서 물려 받은 비권장 클래스다.std::unique_ptr로 대체한 후 완전히 잊는 것이 좋다.
👉🏻 항목 18: 소유권 독점 자원의 관리에는 std::unique_ptr를 사용하라
🔍 std::unique_ptr란?
std::unique_ptr는 독점적 소유권(exclusive ownership) 개념을 구현하고 있다.- 기본적으로 생 포인터와 같은 크기이며, 대부분의 연산(역참조 포함)에서 생 포인터와 정확히 동일한 명령들을 실행한다.
- 이동 전용(move-only) 타입이다. 복사는 허용되지 않는다.
std::unique_ptr를 이동하면 소유권이 원본 포인터에서 대상 포인터로 옮겨진다.- 원본 포인터는 널로 설정된다.
- 널이 아닌
std::unique_ptr는 소멸 시 자신이 가리키는 자원을 파괴한다. 기본적으로 자원 파괴는std::unique_ptr안에 있는 생 포인터에delete를 적용함으로써 수행된다.
🏭 팩터리 함수의 반환 타입으로 활용
std::unique_ptr의 흔한 용도 중 하나는 클래스 계층구조 안의 객체를 생성하는 팩터리 함수의 반환 타입으로 쓰이는 것이다.
class Investment { … };
class Stock : public Investment { … };
class Bond : public Investment { … };
class RealEstate : public Investment { … };
template<typename... Ts>
std::unique_ptr<Investment> // 주어진 인수들로 생성한 객체를 가리키는
makeInvestment(Ts&&... params); // std::unique_ptr를 돌려줌
{
…
auto pInvestment = // pInvestment의 타입은
makeInvestment( 인수들 ); // std::unique_ptr<Investment>
…
} // *pInvestment가 파괴된다
- 클라이언트는 삭제 시 특별한 처리가 필요한 자원을 다루고 있음을 알지 못하고 알 필요도 없다.
std::unique_ptr가 파괴가 정확히 한 번만 일어남을 보장한다.
🗑️ 커스텀 삭제자(Custom Deleter)
기본 삭제자(delete) 대신 커스텀 삭제자를 지정할 수 있다.
auto delInvmt = [](Investment* pInvestment) // 커스텀 삭제자
{ // (람다 표현식)
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)> // 반환 타입이 바뀌었음
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt); // 돌려줄 포인터
if ( /* Stock 객체를 생성해야 하는 경우 */ )
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( /* Bond 객체를 생성해야 하는 경우 */ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( /* RealEstate 객체를 생성해야 하는 경우 */ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
- 커스텀 삭제자는
std::unique_ptr의 둘째 타입 인수로 지정한다. - 생 포인터(
new로 얻은 포인터)를std::unique_ptr에 배정하는 문장은 컴파일되지 않는다. 생 포인터에서 똑똑한 포인터로의 암묵적 변환이 성립하지 않기 때문이다. 따라서reset을 사용한다. - 각
new호출에서는std::forward를 사용해 인수들을 완벽하게 전달한다. (항목 25 참고) - 커스텀 삭제자 람다 표현식 안에서
Investment*로delete된다. 따라서 부모 클래스, 즉Investment의 소멸자가 가상 소멸자이어야 한다.
class Investment {
public:
…
virtual ~Investment(); // 필수적인 설계 요소!
…
};
C++14에서의 간결한 구현:
template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14
{
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};
std::unique_ptr<Investment, decltype(delInvmt)>
pInv(nullptr, delInvmt);
if ( … ) { pInv.reset(new Stock(std::forward<Ts>(params)...)); }
else if ( … ) { pInv.reset(new Bond(std::forward<Ts>(params)...)); }
else if ( … ) { pInv.reset(new RealEstate(std::forward<Ts>(params)...)); }
return pInv;
}
📏 크기 고려사항
- 기본 삭제자(
delete)를 사용할 때에는std::unique_ptr객체의 크기가 생 포인터의 크기와 같다고 가정하는 것이 합당하다. - 커스텀 삭제자를 사용하면 크기가 달라진다.
- 함수 포인터를 삭제자로 지정한 경우:
std::unique_ptr의 크기가 1워드에서 2워드로 증가한다. - 상태 없는 함수 객체(람다 표현식 등): 크기 변화가 없으므로, 삭제자를 보통의 함수로 구현할 수도 있고 람다 표현식으로 구현할 수 있는 경우라면 람다 쪽을 선호하는 것이 바람직하다.
- 함수 포인터를 삭제자로 지정한 경우:
🔀 std::unique_ptr의 두 가지 형태
std::unique_ptr<T>: 개별 객체를 위한 것. 인덱스 연산자(operator[])를 제공하지 않는다.std::unique_ptr<T[]>: 배열을 위한 것. 역참조 연산자(operator*와operator->)를 제공하지 않는다.
배열용
std::unique_ptr는 그냥 지적인 흥미 거리 정도로만 받아들이기 바란다.std::array,std::vector,std::string이 거의 항상 더 나은 선택이기 때문이다.
🔄 std::shared_ptr로의 변환
std::unique_ptr는 std::shared_ptr로의 변환이 쉽고도 효율적이다.
std::shared_ptr<Investment> sp =
makeInvestment( 인수들 ); // std::unique_ptr를 std::shared_ptr로 변환
- 팩터리 함수가
std::unique_ptr를 반환한다면 호출자는 가장 효율적인 똑똑한 포인터를 얻게 되며, 게다가 그것을 좀 더 유연한std::shared_ptr로 변환할 수 있는 여지도 생긴다.
🧐 정리
std::unique_ptr는 독점 소유권 개념을 따르는 자원의 관리를 위한, 작고 빠른 이동 전용 똑똑한 포인터다.- 기본적으로 자원 파괴는
delete를 통해 일어나나, 커스텀 삭제자를 지정할 수 있다. 상태 있는 삭제자나 함수 포인터를 사용하면std::unique_ptr객체의 크기가 커진다. std::unique_ptr를std::shared_ptr로 손쉽게 변환할 수 있다.
댓글남기기