std::unique_ptr
클래스 템플릿 의 인스턴스에 의해 메모리가 관리되는 객체에 포인터를 전달하는 다른 실행 가능한 모드를 설명하려고합니다 . 이전 std::auto_ptr
클래스 템플릿 에도 적용됩니다 (독특한 포인터가 사용하는 모든 사용을 허용하지만 rvalue가 호출 될 필요없이 수정 가능한 lvalue가 허용됩니다 std::move
) 및 어느 정도 적용됩니다 std::shared_ptr
.
토론의 구체적인 예로서 다음과 같은 간단한 목록 유형을 고려할 것입니다.
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
이러한 목록의 인스턴스 (다른 인스턴스와 부품을 공유하거나 순환 할 수 없음)는 최초 list
포인터를 소유 한 사람이 전적으로 소유합니다 . 클라이언트 코드가 저장하는 목록이 절대로 비어 있지 않다는 것을 클라이언트 코드가 알면 첫 번째가 node
아닌을 직접 저장하도록 선택할 수도 있습니다 list
. 소멸자를 node
정의 할 필요가 없습니다. 필드의 소멸자가 자동으로 호출되므로 초기 포인터 또는 노드의 수명이 끝나면 스마트 포인터 소멸자가 전체 목록을 재귀 적으로 삭제합니다.
이 재귀 유형은 일반 데이터에 대한 스마트 포인터의 경우 덜 눈에 띄는 일부 사례를 논의 할 수있는 기회를 제공합니다. 또한 함수 자체는 때때로 클라이언트 코드의 예를 (재귀 적으로) 제공합니다. 대한 형식 정의가 list
편중 물론입니다 unique_ptr
만, 정의는 사용 변경 될 수 있습니다 auto_ptr
또는 shared_ptr
대신 (특히 쓰기 소멸자 할 필요없이 보장되는 예외 안전에 관한) 아래 말했다 것과 변화에 더없이.
스마트 포인터를 전달하는 모드
모드 0 : 스마트 포인터 대신 포인터 또는 참조 인수 전달
함수가 소유권과 관련이없는 경우이 방법이 선호되는 방법입니다. 스마트 포인터를 사용하지 마십시오. 이 경우 함수는 대상 을 소유 한 사람 또는 소유권이 관리되는 수단에 대해 걱정할 필요가 없으므로 원시 포인터를 전달하는 것은 소유권에 관계없이 클라이언트가 항상 할 수 있기 때문에 완벽하게 안전하고 가장 유연한 형태입니다. get
메소드 를 호출 하거나 주소 연산자에서 원시 포인터를 생성하십시오 &
.
예를 들어 그러한 목록의 길이를 계산하는 함수는 list
인수가 아니라 원시 포인터를 제공해야합니다 .
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
변수를 보유한 클라이언트는 list head
이 함수를로 호출 할 수 length(head.get())
있지만 node n
비어 있지 않은 목록을 나타내는 대신 클라이언트를 선택 하면 호출 할 수 있습니다 length(&n)
.
포인터가 null이 아닌 것으로 보장되면 (여기서는 목록이 비어있을 수 있으므로) 포인터가 아닌 참조를 전달하는 것이 좋습니다. const
함수가 노드의 내용을 추가하거나 제거하지 않고 노드의 내용을 업데이트해야하는 경우 비 소유자에 대한 포인터 / 참조 일 수 있습니다 .
모드 0 범주에 속하는 흥미로운 경우는 목록의 (심층) 복사입니다. 이 작업을 수행하는 기능은 물론 사본의 소유권을 이전해야하지만 사본의 목록 소유권과는 관련이 없습니다. 따라서 다음과 같이 정의 할 수 있습니다.
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
가에 (전혀 재귀 호출의 결과를 컴파일 이유에 대한 질문에 대해 모두이 코드는 장점을 자세히 살펴, copy
의 이동 생성자의를 rvalue 참조 인수에 initialiser 목록 바인딩에 unique_ptr<node>
일명, list
는 초기화하는 경우 next
의 필드 생성 된 node
) 및 예외 안전 이유에 대한 질문 (재귀 할당 프로세스 중에 메모리가 부족하고 new
던지기 호출이 발생 std::bad_alloc
하는 경우 부분적으로 구성된 목록에 대한 포인터는 임시 유형의 익명으로 유지됩니다) list
이니셜 라이저 목록에 대해 생성되고 소멸자가 해당 부분 목록을 정리합니다. 그건 그렇고 우리는 (처음에했던 것처럼) 두 번째를 대체하려는 유혹에 저항해야 nullptr
합니다.p
이 시점에서 결국 null 인 것으로 알려져 있습니다 .null이라고 알려진 경우에도 (raw) 포인터 에서 constant에 대한 스마트 포인터를 생성 할 수 없습니다 .
모드 1 : 스마트 포인터를 값으로 전달
인수로 스마트 포인터 값을 취하는 함수는 바로 지정된 객체를 소유합니다. 호출자가 보유한 스마트 포인터 (명명 된 변수이든 익명 임시이든)는 함수 입구와 호출자의 인수에 인수 값으로 복사됩니다. 포인터가 널이되었습니다 (임시의 경우 사본이 생략되었을 수 있지만 호출자가 지정된 오브젝트에 대한 액세스 권한을 상실 함). 이 모드 호출을 현금으로 호출하고 싶습니다 . 호출자가 호출 한 서비스에 대해 선불로 지불하고 호출 후 소유권에 대한 환상을 가질 수 없습니다. 이를 명확하게하기 위해 언어 규칙에 따라 호출자는 인수를std::move
스마트 포인터가 변수에 보유 된 경우 (기술적으로 인수가 lvalue 인 경우); 이 경우 (아래 모드 3은 아님)이 함수는 이름에서 제안한대로 값을 변수에서 임시로 이동하여 변수를 null로 둡니다.
호출 된 함수가 무조건 대상을 소유 (필퍼)하는 경우,이 모드를 사용 std::unique_ptr
하거나 std::auto_ptr
소유와 함께 포인터를 전달하는 좋은 방법으로 메모리 누수 위험을 피할 수 있습니다. 그럼에도 불구하고 아래 모드 3이 모드 1보다 선호되지 않는 상황은 거의 없다고 생각합니다. 이런 이유로 나는이 모드의 사용 예를 제공하지 않을 것입니다. (그러나 reversed
모드 1이 적어도 그 기능을 수행한다고 언급 한 모드 3 의 예를 참조하십시오 .) 함수가이 포인터보다 많은 인수를 취하면 모드를 피할 기술적 이유가 더있을 수 있습니다. 1 ( std::unique_ptr
또는 std::auto_ptr
) : 포인터 변수를 전달하는 동안 실제 이동 작업이 수행되므로p
표현에 의해, 다른 논증 (평가 순서가 지정되지 않음)을 평가하는 동안 유용한 값 std::move(p)
을 p
보유 한다고 가정 할 수 없으며 , 이는 미묘한 오류를 야기 할 수있다. 반대로, 모드 3을 사용 p
하면 함수 호출 전에는 아무런 이동도 일어나지 않으므로 다른 인수는를 통해 값에 안전하게 액세스 할 수 있습니다 p
.
와 함께 사용하면 std::shared_ptr
이 모드는 단일 함수 정의를 사용하면 호출자가 함수에서 사용할 새 공유 복사본을 만드는 동안 포인터의 공유 복사본을 유지할지 여부를 선택할 수 있다는 점에서 흥미 롭습니다 (lvalue 일 때 발생합니다) 호출에 사용 된 공유 포인터에 대한 복사 생성자는 참조 카운트를 증가시킵니다) 또는 참조 카운트를 유지하거나 참조 카운트를 건드리지 않고 포인터 사본을 함수에 제공하기 위해 (rvalue 인수가 제공 될 때 발생합니다. std::move
) 의 호출에 래핑 된 lvalue 예를 들어
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
첫 번째 버전이 복사 시맨틱을 호출한다는 점 (사용시 복사 구성 / 할당 사용 ) 만 다른 기능 본문을 사용하여 ( void f(const std::shared_ptr<X>& x)
lvalue 경우) 및 void f(std::shared_ptr<X>&& x)
(rvalue 경우) 를 별도로 정의하여 동일하게 수행 할 수 x
있지만 두 번째 버전 이동 의미는 ( std::move(x)
예제 코드에서와 같이 작성). 따라서 공유 포인터의 경우 모드 1은 일부 코드 중복을 피하는 데 유용 할 수 있습니다.
모드 2 : (수정 가능) lvalue 참조로 스마트 포인터 전달
여기서 함수는 스마트 포인터에 대한 수정 가능한 참조가 필요하지만 그 기능으로 수행 할 작업에 대한 표시는 제공하지 않습니다. 이 방법 을 카드로 호출하고 싶습니다 . 발신자는 신용 카드 번호를 제공하여 지불을 보장합니다. 참조 는 지정된 대상의 소유권을 얻는 데 사용될 수 있지만 반드시 그럴 필요는 없습니다. 이 모드는 함수의 원하는 효과가 인수 변수에 유용한 값을 남기는 것을 포함 할 수 있다는 사실에 대응하여 수정 가능한 lvalue 인수를 제공해야합니다. 이러한 함수에 전달하고자하는 rvalue 표현식을 가진 호출자는 언어를 상수로 암시 적으로 변환 만 제공하기 때문에 호출 할 수 있도록 명명 된 변수에 저장해야합니다.rvalue에서 lvalue 참조 (임시 참조). (에 의해 처리되는 반대 상황과 달리 스마트 포인터 유형의을 ( 를 ) std::move
캐스트 할 수는 없지만 실제로 원한다면 간단한 템플릿 함수 로이 변환을 얻을 수 있습니다 ( https://stackoverflow.com/a/24868376 참조) / 1436796 ). 호출 된 함수가 인수를 훔쳐서 객체의 소유권을 무조건적으로 가져 오려는 경우, lvalue 인수를 제공해야하는 의무는 잘못된 신호를 제공하는 것입니다. 변수는 호출 후 유용한 값을 갖지 않습니다. 따라서 함수 내에서 동일한 가능성을 제공하지만 호출자에게 rvalue를 제공하도록 요청하는 모드 3은 이러한 사용법에 적합해야합니다.Y&&
Y&
Y
그러나 모드 2의 올바른 사용 사례, 즉 포인터를 수정할 수 있는 함수 또는 소유권과 관련된 방식으로 가리키는 객체가 있습니다. 예를 들어, 노드 앞에 접두사를 붙이는 함수 list
는 이러한 사용의 예를 제공합니다.
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
std::move
스마트 포인터가 이전과는 다르지만 여전히 호출 후 잘 정의되고 비어 있지 않은 목록을 소유하고 있기 때문에 호출자가 강제로 사용하는 것은 바람직하지 않습니다 .
prepend
빈 메모리 부족으로 인해 호출이 실패하면 어떻게되는지 관찰하는 것이 흥미 롭습니다 . 그런 다음 new
전화를 던질 것이다 std::bad_alloc
; 이 시점에서, node
할당 될 수 없었기 때문에 , 전달 된 rvalue 기준 (모드 3)은 std::move(l)
아직 할당 되지 않은 next
필드 를 구성하기 위해 수행 될 것이므로 아직 절제 될 수 없었을 것이다 node
. 따라서 l
오류가 발생해도 원래 스마트 포인터 는 원래 목록을 유지합니다. 그 목록은 스마트 포인터 소멸자에 의해 올바르게 파괴되거나 l
충분히 초기 catch
조항으로 인해 생존 해야하는 경우 원래 목록을 유지합니다.
그것은 건설적인 예였습니다. 이 질문에 대한 윙크로 주어진 값을 포함하는 첫 번째 노드를 제거하는 더 파괴적인 예제를 제공 할 수도 있습니다.
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
여기서도 정확성은 매우 미묘합니다. 특히, 마지막 문장에서 (*p)->next
제거 할 노드 내부에 보유 된 포인터 는 연결이 해제됩니다 (에 의해 release
포인터를 반환하지만 원래 null을 만듭니다)하여 노드를 파괴 하기 전에 reset
(내재적으로) 이전 값을 파괴 할 때 p
) 한 번에 하나의 노드 만 파괴됩니다. (의견에서 언급 된 대안적인 형태에서,이 타이밍은 std::unique_ptr
인스턴스 의 이동 할당 연산자의 구현 내부에 list
맡겨져 있으며, 표준은 20.7.1.2.3; 2에 따르면이 연산자는 " 호출 reset(u.release())
타이밍이 너무 여기에 안전해야 어디서, ".)
참고 prepend
및 remove_first
로컬 저장 클라이언트에서 호출 할 수 없습니다 node
항시 비어 있지 않은 목록 변수를 바르게 그래서 구현은 이러한 경우에 할 수없는 작업을 부여하기 때문이다.
모드 3 : (수정 가능) rvalue 참조로 스마트 포인터 전달
이것은 단순히 포인터의 소유권을 가질 때 사용하는 기본 모드입니다. 이 메소드 호출을 수표 로 호출하고 싶습니다 . 호출자는 수표 에 서명하여 현금을 제공하는 것처럼 소유권을 포기해야하지만 호출 된 함수가 실제로 포인터를 pilfer 할 때까지 실제 철수는 연기됩니다 (정확히 모드 2를 사용할 때와 동일) ). "체크 서명"은 구체적으로 호출자가 std::move
lvalue 인 경우 (모드 1에서와 같이) 인수를 랩핑해야한다는 것을 의미 합니다 (rvalue 인 경우 "소유권 부여"부분은 명백하며 별도의 코드가 필요하지 않습니다).
기술적으로 모드 3은 모드 2와 정확히 동일하게 작동하므로 호출 된 함수 는 소유권을 가질 필요가 없습니다 . 그러나 나는 (정상 사용에서) 소유권 이전에 대한 불확실성이있는 경우, 모드 2 모드 3를 사용하여 그들이 그 호출자에 신호 암시입니다 그래서, 모드 3 선호해야한다고 주장하는 것이 된다 소유권을 포기은. 모드 1 인수 만 전달하면 실제로 소유자에게 강제 소유권 손실을 신호한다는 사실을 알 수 있습니다. 그러나 클라이언트가 호출 된 함수의 의도에 대해 의문이있는 경우 호출되는 함수의 스펙을 알고 있어야합니다.
list
모드 3 인수 전달을 사용 하는 유형 과 관련된 전형적인 예를 찾는 것은 놀랍게도 어렵습니다 . b
다른 목록의 끝으로 목록 을 이동하는 a
것이 일반적인 예입니다. 그러나 a
(작동 결과를 유지하고 유지하는) 모드 2를 사용하는 것이 좋습니다.
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
모드 3 인수 전달의 순수한 예는 다음과 같습니다 (목록 및 소유권). 동일한 노드를 포함하는 목록을 역순으로 리턴합니다.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
이 함수 l = reversed(std::move(l));
는 목록을 자체로 뒤집기 위해 in에서와 같이 호출 될 수 있지만 반전 된 목록도 다르게 사용할 수 있습니다.
여기서 인수는 효율성을 위해 즉시 로컬 변수로 이동합니다 (매개 변수 l
는 대신에 매개 변수를 직접 사용할 수 p
있지만 매번 액세스하면 여분의 간접 수준이 필요합니다). 따라서 모드 1 인수 전달과의 차이는 최소화됩니다. 실제로이 모드를 사용하면 인수가 로컬 변수로 직접 제공되어 초기 이동을 피할 수 있습니다. 이것은 참조에 의해 전달 된 인수가 로컬 변수를 초기화하는 역할 만하는 경우, 값으로 전달하는 대신 매개 변수를 로컬 변수로 사용할 수 있다는 일반적인 원칙의 예일뿐입니다.
모드 3을 사용하여 스마트 포인터의 소유권을 전달하는 모든 제공된 라이브러리 함수가 모드 3을 사용하여 제공한다는 사실에서 알 수 있듯이 모드 3 사용은 표준에 의해 옹호되는 것으로 보입니다 std::shared_ptr<T>(auto_ptr<T>&& p)
. 이 생성자는 (in std::tr1
)을 사용하여 수정 가능한 lvalue 참조 ( auto_ptr<T>&
복사 생성자 와 마찬가지로 )를 가져 왔 으므로와 같이 auto_ptr<T>
lvalue p
로 호출 할 수 있으며 std::shared_ptr<T> q(p)
그 후에 p
는 null로 재설정됩니다. 인수 전달에서 모드 2에서 3으로 변경되었으므로이 이전 코드를 다시 작성하고 std::shared_ptr<T> q(std::move(p))
계속 작동해야합니다. 나는위원회가 여기에서 모드 2를 좋아하지 않는다는 것을 이해하지만, 그들은std::shared_ptr<T>(auto_ptr<T> p)
대신 (독특한 포인터와 달리) 자동 포인터는 값 (포인터 객체 자체가 프로세스에서 null로 재설정 됨)을 자동으로 역 참조 할 수 있기 때문에 오래된 코드가 수정없이 작동하도록 할 수있었습니다. 위원회는 모드 1보다 옹호 모드 3을 훨씬 선호했기 때문에 이미 사용이 중단 된 경우에도 모드 1을 사용하지 않고 기존 코드 를 적극적으로 중단 하기로 결정했습니다 .
모드 1보다 모드 3을 선호하는 경우
모드 1은 많은 경우에 완벽하게 사용할 수 있으며 소유권을 가정 할 때 reversed
위 의 예 에서와 같이 스마트 포인터를 로컬 변수로 이동하는 형식을 취하는 경우 모드 3보다 선호 될 수 있습니다 . 그러나 더 일반적인 경우 모드 3을 선호하는 두 가지 이유를 알 수 있습니다.
임시를 만드는 것보다 참조를 전달하고 기존 포인터를 닉스하는 것이 약간 더 효율적입니다 (현금 처리는 다소 힘들다). 일부 시나리오에서는 포인터가 실제로 제거되기 전에 다른 함수로 변경되지 않은 채 여러 번 전달 될 수 있습니다. 이러한 전달에는 일반적으로 쓰기가 필요 std::move
하지만 (모드 2를 사용하지 않는 한) 실제로는 아무것도 수행하지 않는 (특히 역 참조가없는) 캐스트이므로 비용이 전혀 들지 않습니다.
함수 호출의 시작과 함수 호출 (또는 포함 된 호출)이 실제로 지정된 객체를 다른 데이터 구조로 이동시키는 지점 사이에 예외가 발생한다고 생각할 수 있습니다 (이 예외는 함수 자체에서 이미 포착되지 않았습니다) ), 모드 1을 사용할 때 스마트 포인터가 참조하는 객체는 catch
절이 예외를 처리 하기 전에 파괴됩니다 (스택 해제 동안 함수 매개 변수가 파괴되었으므로). 모드 3을 사용할 때는 그렇지 않습니다. 호출자에게는 이러한 경우 (예외를 포착하여) 객체의 데이터를 복구 할 수있는 옵션이 있습니다. 여기서 모드 1 은 메모리 누수를 유발하지 않지만 프로그램의 데이터를 복구 할 수없는 손실로 이어질 수 있으며, 이는 바람직하지 않을 수도 있습니다.
스마트 포인터 반환 : 항상 가치
스마트 포인터 를 반환 하는 것에 관한 단어를 결론 지 으려면 아마도 호출자가 사용하기 위해 만든 객체를 가리킬 것입니다. 이것은 실제로 포인터를 함수에 전달하는 것과 비교할 수없는 경우는 아니지만 완전성을 위해 항상 값으로 반환 한다고 주장하고 싶습니다 ( 문 에서 사용하지 마십시오 ). 아무도 방금 수정 된 포인터에 대한 참조 를 원하지 않습니다.std::move
return