소유권과 스마트 포인터 (Ownership and Smart Pointers)¶
동적으로 할당한 객체에는 하나의 고정된 소유자를 두는 것을 선호하세요. 소유권은 스마트 포인터로 넘기는 것을 선호하세요.
정의:
"소유권"은 동적으로 할당한 메모리(그리고 그 밖의 리소스)를 관리하기 위한 장부 관리 기법입니다. 동적으로 할당한 객체의 소유자란, 그 객체가 더 이상 필요 없을 때 삭제되도록 보장할 책임을 지는 객체나 함수를 말합니다. 소유권은 공유될 수도 있는데, 그런 경우에는 보통 마지막 소유자가 삭제할 책임을 집니다. 소유권이 공유되지 않을 때도 한 코드에서 다른 코드로 이전될 수 있습니다.
"스마트" 포인터는 예를 들어 *와 -> 연산자를 오버로드해서 포인터처럼 동작하는 클래스입니다. 일부 스마트 포인터 타입은 소유권 장부 관리를 자동화해 이 책임이 지켜지도록 하는 데 쓸 수 있습니다. std::unique_ptr은 동적으로 할당한 객체에 대한 배타적 소유권을 표현하는 스마트 포인터 타입입니다. std::unique_ptr이 범위를 벗어나면 그 객체는 삭제됩니다. 복사할 수는 없지만, 소유권 이전을 나타내기 위해 이동할 수는 있습니다. std::shared_ptr은 동적으로 할당한 객체에 대한 공유 소유권을 표현하는 스마트 포인터 타입입니다. std::shared_ptr은 복사할 수 있습니다. 객체의 소유권은 모든 복사본이 함께 가지며, 마지막 std::shared_ptr이 소멸될 때 객체가 삭제됩니다.
장점:
- 어떤 형태로든 소유권 논리 없이 동적으로 할당한 메모리를 관리하는 것은 사실상 불가능합니다.
- 객체의 소유권을 넘기는 것이 그것을 복사하는 것보다 저렴할 수 있습니다(복사가 가능하기라도 하다면 말입니다).
- 소유권을 넘기는 편이 포인터나 참조를 '빌려주는' 것보다 단순할 수 있습니다. 두 사용자 사이에서 객체의 수명을 맞출 필요가 줄어들기 때문입니다.
- 스마트 포인터는 소유권 논리를 명시적이고 자기 설명적이며 모호하지 않게 만들어 가독성을 높일 수 있습니다.
- 스마트 포인터는 손으로 하는 소유권 장부 관리를 없애 코드를 단순하게 만들고, 큰 부류의 오류들을 아예 배제할 수 있습니다.
const객체라면 공유 소유권이 깊은 복사에 대한 단순하고 효율적인 대안이 될 수 있습니다.
단점:
- 소유권은 (스마트든 일반이든) 포인터를 통해 표현되고 이전되어야 합니다. 포인터 의미론은 값 의미론보다 복잡하며 API에서 특히 그렇습니다. 소유권뿐 아니라 앨리어싱, 수명, 가변성 같은 문제까지 신경 써야 합니다.
- 값 의미론의 성능 비용은 과대평가되는 경우가 많아서, 소유권 이전으로 얻는 성능 이득이 가독성과 복잡성에 드는 비용을 정당화하지 못할 수 있습니다.
- 소유권을 이전하는 API는 그 사용자에게 하나의 메모리 관리 모델을 강요합니다.
- 스마트 포인터를 쓰는 코드는 리소스 해제가 어디서 일어나는지를 덜 분명하게 드러냅니다.
std::unique_ptr은 이동 의미론으로 소유권 이전을 표현하는데, 이는 복잡할 수 있고 일부 프로그래머를 혼란스럽게 할 수 있습니다.- 공유 소유권은 소유권을 신중하게 설계하는 대신 택하고 싶은 솔깃한 대안이 되어, 시스템의 설계를 흐려 놓을 수 있습니다.
- 공유 소유권은 런타임에 명시적인 장부 관리를 요구하며, 그 비용이 클 수 있습니다.
- 어떤 경우에는(예: 순환 참조) 공유 소유권을 가진 객체가 영영 삭제되지 않을 수 있습니다.
- 스마트 포인터가 일반 포인터를 완벽하게 대체하지는 못합니다.
결정:
동적 할당이 필요하다면, 할당한 코드가 소유권을 계속 갖는 편을 선호하세요. 다른 코드가 그 객체에 접근해야 한다면 복사본을 넘기거나, 소유권을 이전하지 않고 포인터나 참조를 넘기는 것을 고려하세요. 소유권 이전을 명시적으로 드러내려면 std::unique_ptr을 쓰는 편을 선호하세요. 예를 들면 다음과 같습니다.
아주 좋은 이유 없이는 공유 소유권을 쓰도록 코드를 설계하지 마세요. 그런 이유 중 하나는 비싼 복사 연산을 피하는 것인데, 성능 이득이 상당하고 대상 객체가 불변일 때(즉 std::shared_ptr<const Foo>일 때)만 그렇게 해야 합니다. 공유 소유권을 쓴다면 std::shared_ptr을 쓰는 편을 선호하세요.
std::auto_ptr은 절대 쓰지 마세요. 대신 std::unique_ptr을 쓰세요.
옮긴이 풀이¶
핵심: 단일 소유자를 선호, 소유권 이전은 스마트 포인터로¶
동적 할당 객체에는 한 명의 명확한 소유자를 두는 것을 선호하세요. 소유권을 넘겨야 한다면 std::unique_ptr로 그 의도를 코드에 드러내세요.
std::unique_ptr<Foo> FooFactory(); // 소유권을 호출자에게 넘김
void FooConsumer(std::unique_ptr<Foo> ptr); // 소유권을 넘겨받음
기본 원칙¶
- 동적 할당이 필요하면, 할당한 코드가 소유권을 유지하세요.
- 다른 코드가 객체에 접근만 해야 한다면, 소유권을 넘기지 말고 복사본이나 포인터·참조를 전달하세요.
- 소유권 이전을 표현할 때만
std::unique_ptr을 쓰세요.
공유 소유권(shared_ptr)은 신중히¶
std::shared_ptr은 마지막 소유자가 사라질 때 객체를 지웁니다. 편리하지만 런타임 장부 비용이 들고, 순환 참조 시 객체가 안 지워질 수 있으며, 설계를 모호하게 만듭니다. 비싼 복사를 피하고 이득이 크며 대상이 불변일 때(std::shared_ptr<const Foo>)만 쓰세요.
금지¶
std::auto_ptr은 쓰지 마세요. std::unique_ptr을 쓰세요.