unique_ptr 인수를 생성자 또는 함수에 어떻게 전달합니까?


400

C ++ 11에서 의미를 옮기는 것이 처음 unique_ptr이며 생성자 또는 함수에서 매개 변수 를 처리하는 방법을 잘 모릅니다 . 이 클래스 자체를 참조하십시오.

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

이것이 unique_ptr인수를 취하는 함수를 작성하는 방법 입니까?

그리고 std::move호출 코드에서 사용해야 합니까?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
빈 포인터에서 b1-> setNext를 호출 할 때 세그먼트 오류가 아닙니까?
balki

답변:


836

다음은 고유 한 포인터를 인수로 사용할 수있는 가능한 방법과 관련 의미입니다.

(A) 가치

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

사용자가 전화를 걸려면 다음 중 하나를 수행해야합니다.

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

값으로 고유 한 포인터를 사용한다는 것은 포인터의 소유권을 해당 함수 / 객체 등으로 전송 한다는 의미입니다 . 후 newBase구성되어, nextBase보장되는 . 객체를 소유하지 않으며 더 이상 객체에 대한 포인터가 없습니다. 그것은 사라 졌어요.

이는 매개 변수를 값으로 가져 오기 때문에 보장됩니다. std::move실제로 아무것도 움직이지 않습니다 . 그냥 멋진 캐스팅입니다. std::move(nextBase)Base&&대한 r- 값 참조 인 a 를 반환합니다 nextBase. 그게 다야.

Base::Base(std::unique_ptr<Base> n)r- 값 참조가 아닌 값으로 인수를 취하기 때문에 C ++은 자동으로 임시를 구성합니다. 를 통해 함수를 제공 한 std::unique_ptr<Base>에서를 만듭니다 . 실제로 값을 함수 인수로 옮기는 것이이 임시 구성입니다 .Base&&std::move(nextBase)nextBasen

(B) 비 상수 l 값 참조로

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

이것은 실제 l- 값 (명명 된 변수)에서 호출되어야합니다. 다음과 같이 임시로 호출 할 수 없습니다.

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

이것의 의미는 비 const 참조의 다른 사용의 의미와 동일합니다. 함수는 포인터의 소유권을 주장 하거나 주장 하지 않을 수 있습니다 . 이 코드가 주어지면 :

Base newBase(nextBase);

nextBase비어 있다는 보장은 없습니다 . 그것은 수 있습니다 비어; 그렇지 않을 수 있습니다. 실제로하고 Base::Base(std::unique_ptr<Base> &n)싶은 일에 달려 있습니다. 그 때문에 함수 시그너처에서 무슨 일이 벌어 질지 분명하지 않습니다. 구현 (또는 관련 문서)을 읽어야합니다.

이 때문에 인터페이스로 제안하지 않습니다.

(C) const l-value reference로

Base(std::unique_ptr<Base> const &n);

에서 이동할 없기 때문에 구현을 표시하지 않습니다 const&. 를 전달 const&하면 함수가 Base포인터를 통해 비아에 액세스 할 수 있지만 어디에도 저장할 수는 없습니다 . 소유권을 주장 할 수 없습니다.

유용 할 수 있습니다. 귀하의 특정 경우에 반드시 해당되는 것은 아니지만, 누군가에게 포인터를 건네 줄 수 있고 (캐스팅을하지 않는 것처럼 C ++의 규칙을 어 기지 않고 const) 소유권을 주장 할 수 없다는 것을 항상 알고있는 것이 좋습니다. 그들은 그것을 저장할 수 없습니다. 그들은 그것을 다른 사람들에게 넘길 수 있지만, 다른 사람들도 같은 규칙을 따라야합니다.

(D) r- 값 기준

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

이것은 "비 상수 l- 값 참조에 의한"경우와 거의 동일합니다. 차이점은 두 가지입니다.

  1. 임시로 전달할 수 있습니다 .

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. 당신은 있어야 사용 std::move이 아닌 일시적 인수를 전달할 때.

후자는 실제로 문제입니다. 이 줄이 보이면 :

Base newBase(std::move(nextBase));

이 행이 완료된 후에 nextBase는 비어 있어야 한다는 합리적인 기대 가 있습니다. 이동 했어야합니다. 결국, 당신은 std::move그 운동이 일어났다 고 말하면서 거기에 앉아 있습니다.

문제는 그렇지 않은 것입니다. 옮겼다는 보장 은 없습니다 . 그것은 에서 이동 된,하지만 당신은 단지 소스 코드를 보면 알 수 있습니다. 함수 서명만으로는 알 수 없습니다.

추천

  • 값으로 (A) : 당신이 주장에 기능에 대한 의미하는 경우 소유권unique_ptr, 값으로 가져 가라.
  • (C) const l-value reference : 함수가 단순히 unique_ptr함수 실행 기간 동안 단순히를 사용한다는 것을 의미한다면을 사용하십시오 const&. &또는 const&을 사용하지 말고을 가리키는 실제 유형 으로 또는 을 전달 하십시오 unique_ptr.
  • (D) r- 값 참조 : 함수가 (내부 코드 경로에 따라) 소유권을 주장 할 수도 있고 청구하지 않을 수도 있습니다 &&. 그러나 가능할 때마다이 작업을 수행하지 않는 것이 좋습니다.

unique_ptr을 조작하는 방법

를 복사 할 수 없습니다 unique_ptr. 당신은 그것을 이동할 수 있습니다. 이를위한 올바른 방법은 std::move표준 라이브러리 기능을 사용하는 것입니다.

당신이 걸릴 경우 unique_ptr값에 의해, 당신은 자유롭게 이동할 수 있습니다. 그러나 실제로는 운동이 발생하지 않습니다 std::move. 다음 진술을 보자.

std::unique_ptr<Base> newPtr(std::move(oldPtr));

이것은 실제로 두 가지 진술입니다.

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(참고 : 비 임시 r- 값 참조는 실제로 r- 값이 아니므로 위의 코드는 기술적으로 컴파일되지 않습니다. 데모 목적으로 만 사용됩니다).

temporary단지에 R 값의 기준이다 oldPtr. 움직임이 발생 하는 곳 의 생성자newPtr있습니다. unique_ptr의 이동 생성자 ( &&자체 를 취하는 생성자 )는 실제 이동을 수행합니다.

당신이있는 경우 unique_ptr값을 당신이 어딘가에 저장하려면, 당신은 해야한다 사용하는 std::move저장을 할 수 있습니다.


5
@Nicol : std::move반환 값의 이름을 지정하지 않습니다. 명명 된 rvalue 참조는 lvalue입니다. ideone.com/VlEM3
R. Martinho Fernandes

31
나는 기본적 으로이 답변에 동의하지만 몇 가지 언급이 있습니다. (1) const lvalue에 대한 참조를 전달하는 유효한 유스 케이스가 없다고 생각합니다. 수신자가 할 수있는 모든 것, const (bare) 포인터에 대한 참조로도 가능하거나 포인터 자체를 개선 할 수도 있습니다. 소유권이 전체적으로 유지된다는 것을 아는 것은 그 사업의 어느 것도 아니다 unique_ptr. 어쩌면 다른 발신자가 같은 기능이 필요하지만이 들고 shared_ptr함수를 호출하는 경우 대신] 좌변 참조로 (2) 호출이 유용 할 수 수정을 포인터, 예를 들어, 추가 또는 링크 된 목록에서 (목록 소유) 노드를 제거.
Marc van Leeuwen

8
... (3) rvalue 참조로 전달하는 것보다 값으로 전달하는 것을 선호하는 주장이 의미가 있지만 표준 자체는 항상 unique_ptrrvalue 참조로 값을 전달한다고 생각합니다 (예 : 값을로 변환 할 때 shared_ptr). 그 이유는 호출자에게 똑같은 권한을 부여하는 동안 (임시 포인터로 이동하지 않음) 약간 더 효율적이라는 것입니다 (rvalue 또는 lvalue를 감싸지 std::move만 벌거 벗은 lvalue 는 전달할 수 없음).
Marc van Leeuwen

19
Marc가 말한 것을 반복하고 Sutter를 인용 하자면 : "const unique_ptr &을 매개 변수로 사용하지 말고 widget *을 대신 사용하십시오"
Jon

17
우리는 값에 의한 문제를 발견 했습니다. 인수 초기화 중에 이동이 발생합니다. 이는 다른 인수 평가와 관련하여 순서가 맞지 않습니다 (물론 initializer_list는 제외). rvalue 참조를 승인하는 것은 함수 호출 후 및 다른 인수 평가 후 이동이 발생하도록 강력하게 지시합니다. 따라서 소유권을 가질 때마다 rvalue 참조를 수락하는 것이 좋습니다.
Ben Voigt

57

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())타이밍이 너무 여기에 안전해야 어디서, ".)

참고 prependremove_first로컬 저장 클라이언트에서 호출 할 수 없습니다 node항시 비어 있지 않은 목록 변수를 바르게 그래서 구현은 이러한 경우에 할 수없는 작업을 부여하기 때문이다.

모드 3 : (수정 가능) rvalue 참조로 스마트 포인터 전달

이것은 단순히 포인터의 소유권을 가질 때 사용하는 기본 모드입니다. 이 메소드 호출을 수표 로 호출하고 싶습니다 . 호출자는 수표 에 서명하여 현금을 제공하는 것처럼 소유권을 포기해야하지만 호출 된 함수가 실제로 포인터를 pilfer 할 때까지 실제 철수는 연기됩니다 (정확히 모드 2를 사용할 때와 동일) ). "체크 서명"은 구체적으로 호출자가 std::movelvalue 인 경우 (모드 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::movereturn


1
모드 0의 경우 +1-unique_ptr 대신 기본 포인터를 전달합니다. 주제에서 약간 벗어난 주제 (질문은 unique_ptr 전달에 관한 것이므로) 간단하지만 문제를 피합니다.
Machta

" 여기서 모드 1은 메모리 누수를 일으키지 않습니다. "-모드 3이 메모리 누수를 일으킨다는 것을 의미합니다. 이는 사실이 아닙니다. unique_ptr이동 여부에 관계없이 파괴되거나 재사용 될 때마다 값을 유지하면 값을 삭제합니다.
rustyx

@ RusstyX : 나는 당신이 그 의미를 어떻게 해석하는지 알 수 없으며, 내가 암시한다고 생각하는 것을 말하려고하지 않았습니다. 다른 곳에서 사용 unique_ptr하면 메모리 누수 를 방지하고 (따라서 어떤 의미에서는 계약을 이행 함) 여기서 (예 : 모드 1 사용) 더 해로운 것으로 간주 될 수있는 특정 상황에서 발생할 수 있습니다 데이터, 즉 손실 모드 3. 사용하여 피할 수있는 (가치의 파괴에 지적)
마크 반 리웬을

4

unique_ptr, 생성자에서 by 값 을 가져 가야합니다 . 명백 함은 좋은 것입니다. unique_ptr복사 할 수 없기 때문에 (비공개 사본 ctor) 작성한 내용에 컴파일러 오류가 발생합니다.


3

편집 : 이 답변은 엄격하게 말하면 코드가 작동하지만 잘못되었습니다. 토론은 너무 유용하기 때문에 여기에만 남겨두고 있습니다. 이 다른 대답은 내가 이것을 마지막으로 편집했을 때 주어진 가장 좋은 대답입니다 . unique_ptr 인수를 생성자 또는 함수에 어떻게 전달합니까?

기본 아이디어는 ::std::move당신을 지나가는 사람들이 자신이 지나가는 사람들이 소유권을 잃을 것이라는 unique_ptr지식을 표현하기 위해 그것을 사용해야한다는 것 unique_ptr입니다.

unique_ptr, 메소드 unique_ptr자체가 아닌 메소드에 rvalue 참조를 사용해야 함을 의미합니다 . 평범한 구식으로 전달 unique_ptr하면 사본을 만들어야하고에 대한 인터페이스에서 명시 적으로 금지되어 있기 때문에 어쨌든 작동하지 않습니다 unique_ptr. 흥미롭게도 명명 된 rvalue 참조를 사용하면이를 다시 lvalue로 변환하므로 메소드 ::std::move 내부 에서도 사용해야 합니다.

이것은 두 가지 방법이 다음과 같아야 함을 의미합니다.

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

그런 다음 방법을 사용하는 사람들은 다음을 수행합니다.

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

보시다시피 ::std::move포인터가 가장 관련성이 있고 도움이되는 시점에서 포인터가 소유권을 잃을 것임을 나타냅니다. 이런 일이 눈에 띄지 않게된다면, 수업을 이용하는 사람들이 objptr명백한 이유없이 갑자기 소유권을 잃어버린 것은 매우 혼란 스러울 것 입니다.


2
명명 된 rvalue 참조는 lvalue입니다.
R. Martinho Fernandes

확실 Base fred(::std::move(objptr));하지 Base::UPtr fred(::std::move(objptr));않습니까?
codablank1

1
내 이전 의견에 추가하려면이 코드는 컴파일되지 않습니다. 여전히 std::move생성자와 메소드의 구현에 사용해야 합니다. 그리고 값으로 전달하더라도 호출자는 여전히 std::movelvalue를 전달 하는 데 사용해야 합니다. 가장 큰 차이점은 인터페이스가 가치를 전달한다는 점에서 가치를 전달함으로써 소유권을 잃을 것이라는 점입니다. 다른 답변에 대한 Nicol Bolas 의견을 참조하십시오.
R. Martinho Fernandes

@ codablank1 : 예. rvalue 참조를 취하는 기본으로 생성자와 메소드를 사용하는 방법을 시연하고 있습니다.
Omnifarious

@ R.MartinhoFernandes : 아, 흥미 롭습니다. 나는 그것이 의미가 있다고 생각합니다. 나는 당신이 틀릴 것이라고 기대했지만 실제 테스트는 당신이 올바른 것으로 판명되었습니다. 지금 수정했습니다.
Omnifarious

0
Base(Base::UPtr n):next(std::move(n)) {}

로 훨씬 좋아야한다

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

void setNext(Base::UPtr n)

해야한다

void setNext(Base::UPtr&& n)

같은 몸으로.

그리고 ... 무엇 evthandle()?


3
여기서는 아무런 이점 이 없습니다 std::forward. 항상 rvalue 참조 유형이며 rvalue로 전달합니다. 이미 올바르게 전달되었습니다. Base::UPtr&&std::move
R. Martinho Fernandes

7
나는 매우 동의하지 않습니다. 함수가 unique_ptrby 값을 가지면, 이동 생성자가 새로운 값에 대해 호출되었다는 것을 보증 합니다 (또는 단순히 임시 값이 주어 졌음). 이것은 사용자가 가진 변수가 이제 비어 있는지 확인합니다 . 대신 가져 가면 코드에서 이동 작업을 호출 한 경우에만 비워집니다. 당신의 방법으로, 사용자가 이동하지 않은 변수가 가능합니다. 사용자가 용의자와 혼란을 사용하게 만듭니다 . 를 사용 하면 항상 무언가가 움직여야 합니다. unique_ptr&&std::movestd::move
Nicol Bolas

@NicolBolas : 네 말이 맞아. 답변이 작동하는 동안 귀하의 관찰은 정확하기 때문에 답변을 삭제하겠습니다.
Omnifarious

0

최고 투표 답변. rvalue 참조를 전달하는 것을 선호합니다.

rvalue 참조로 전달하면 어떤 문제가 발생할 수 있는지 이해합니다. 그러나이 문제를 양면으로 나누십시오.

  • 발신자 :

나는 코드를 작성해야 Base newBase(std::move(<lvalue>))하거나 Base newBase(<rvalue>).

  • 수신자 :

라이브러리 작성자는 소유권을 소유하려는 경우 실제로 unique_ptr을 이동하여 멤버를 초기화합니다.

그게 다야.

rvalue 참조로 전달하면 하나의 "이동"명령 만 호출하지만 값으로 전달하면 2입니다.

그러나 라이브러리 작성자가 이것에 대해 전문가가 아닌 경우 unique_ptr을 이동하여 멤버를 초기화 할 수는 없지만 저자의 문제는 아닙니다. 값이나 rvalue 참조로 전달되는 코드는 동일합니다!

라이브러리를 작성하는 경우 이제 라이브러리를 보장해야한다는 것을 알고 있으므로 rvalue 참조를 전달하는 것이 value보다 나은 선택입니다. 라이브러리를 사용하는 클라이언트는 동일한 코드를 작성합니다.

자, 당신의 질문에. unique_ptr 인수를 생성자 또는 함수에 어떻게 전달합니까?

최선의 선택이 무엇인지 알고 있습니다.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.