unique_ptr이있는 클래스의 복사 생성자


105

unique_ptr멤버 변수 가있는 클래스에 대한 복사 생성자를 어떻게 구현 합니까? 저는 C ++ 11만을 고려하고 있습니다.


9
음, 복사 생성자가 무엇을하기를 원하십니까?
니콜 올가미

unique_ptr은 복사 할 수 없다는 것을 읽었습니다. 이로 인해 std::vector.
codefx 2013

2
@AbhijitKadam unique_ptr의 내용에 대한 깊은 사본을 만들 수 있습니다. 사실, 그것은 종종 현명한 일입니다.
Cubic

2
잘못된 질문을하고있을 수 있습니다. 을 포함하는 클래스에 대한 복사 생성자를 unique_ptr원하지 않을 것입니다. 목표가 데이터를 std::vector. 반면에 C ++ 11 표준은 자동으로 이동 생성자를 생성했기 때문에 복사 생성자를 원할 수도 있습니다.
Yakk-Adam Nevraumont 2013-04-16

3
@codefx 벡터 요소는 복사 가능할 필요가 없습니다. 그것은 단지 벡터를 복사 할 수 없다는 것을 의미합니다.
MM

답변:


81

이 때문에 unique_ptr공유 할 수 없습니다 중 하나의 내용을 깊은 복사하거나 변환하려면 다음이 필요합니다 unique_ptrA를 shared_ptr.

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_( new int( i ) ) {}
   A( const A& a ) : up_( new int( *a.up_ ) ) {}
};

int main()
{
   A a( 42 );
   A b = a;
}

NPE가 언급했듯이 copy-ctor 대신 move-ctor를 사용할 수 있지만 그 결과 클래스의 의미가 다릅니다. move-ctor는 std::move다음을 통해 명시 적으로 멤버를 이동 가능하게 만들어야합니다 .

A( A&& a ) : up_( std::move( a.up_ ) ) {}

필요한 연산자의 완전한 세트를 갖는 것은 또한

A& operator=( const A& a )
{
   up_.reset( new int( *a.up_ ) );
   return *this,
}

A& operator=( A&& a )
{
   up_ = std::move( a.up_ );
   return *this,
}

에서 클래스를 사용 std::vector하려면 기본적으로 벡터가 객체의 고유 소유자인지 여부를 결정해야합니다.이 경우 클래스를 이동 가능하게 만드는 것으로 충분하지만 복사 할 수는 없습니다. copy-ctor 및 copy-assignment를 생략하면 컴파일러가 이동 전용 유형으로 std :: vector를 사용하는 방법을 안내합니다.


4
이동 생성자를 언급 할 가치가 있습니까?
NPE

4
+1하지만 이동 생성자는 더 강조되어야합니다. 의견에서 OP는 목표가 벡터에서 객체를 사용하는 것이라고 말합니다. 이를 위해 이동 건설과 이동 할당 만 필요한 것입니다.
jogojapan

36
경고로 위의 전략은 int. unique_ptr<Base>를 저장 하는가있는 경우 Derived위의 내용이 슬라이스됩니다.
Yakk-Adam Nevraumont 2013

5
null에 대한 검사가 없으므로 그대로 nullptr 역 참조를 허용합니다. 어때A( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
라이언 이닝

1
@Aaron 다형성 상황에서 삭제자는 유형이 지워지거나 무의미합니다 (삭제 유형을 알고 있다면 왜 삭제 자 만 변경합니까?). 어떤 경우에는, 그래,이는 A의 디자인 value_ptr- unique_ptr플러스 Deleter가 / 복사기 정보.
Yakk-Adam Nevraumont 2015

46

unique_ptr클래스에서 를 갖는 일반적인 경우는 상속을 사용할 수있는 것입니다 (그렇지 않으면 일반 객체도 종종 수행됩니다. RAII 참조). 이 경우 지금까지이 스레드에 적절한 답변이 없습니다 .

따라서 여기에 시작점이 있습니다.

struct Base
{
    //some stuff
};

struct Derived : public Base
{
    //some stuff
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class
};

... 그리고 목표는 말했듯이 Foo복사 가능 하게 만드는 것 입니다.

이를 위해 파생 클래스가 올바르게 복사되도록 포함 된 포인터의 전체 복사 를 수행해야 합니다.

다음 코드를 추가하면됩니다.

struct Base
{
    //some stuff

    auto clone() const { return std::unique_ptr<Base>(clone_impl()); }
protected:
    virtual Base* clone_impl() const = 0;
};

struct Derived : public Base
{
    //some stuff

protected:
    virtual Derived* clone_impl() const override { return new Derived(*this); };                                                 
};

struct Foo
{
    std::unique_ptr<Base> ptr;  //points to Derived or some other derived class

    //rule of five
    ~Foo() = default;
    Foo(Foo const& other) : ptr(other.ptr->clone()) {}
    Foo(Foo && other) = default;
    Foo& operator=(Foo const& other) { ptr = other.ptr->clone(); return *this; }
    Foo& operator=(Foo && other) = default;
};

여기에는 기본적으로 두 가지가 있습니다.

  • 첫 번째는 Foo복사 생성자가 삭제됨에 따라에서 암시 적으로 삭제되는 복사 및 이동 생성자의 추가입니다 unique_ptr. 이동 생성자는 = default...로 간단히 추가 할 수 있습니다 . 이는 컴파일러에게 일반적인 이동 생성자 가 삭제 되지 않아야 함 을 알리기위한 것입니다 ( unique_ptr이 경우에 사용할 수있는 이동 생성자가 이미 있으므로 작동합니다 ).

    의 복사 생성자의 경우 복사 생성자 Foo가없는 것과 유사한 메커니즘이 없습니다 unique_ptr. 따라서 unique_ptr새로운를 생성하고 원래 pointee의 복사본으로 채우고 복사 된 클래스의 구성원으로 사용해야합니다.

  • 상속이 포함 된 경우 원본 포인트의 사본을 신중하게 작성해야합니다. 그 이유는 std::unique_ptr<Base>(*ptr)위의 코드에서 간단한 복사를 수행하면 슬라이스가 발생하기 때문입니다. 즉, 객체의 기본 구성 요소 만 복사되고 파생 된 부분은 누락됩니다.

    이를 방지하려면 복제 패턴을 통해 복사해야합니다. 아이디어는 기본 클래스에서 clone_impl()를 반환하는 가상 함수 를 통해 복사를 수행하는 것입니다 Base*. 그러나 파생 클래스에서는를 반환하도록 공분산을 통해 확장되며이 Derived*포인터는 파생 클래스의 새로 생성 된 복사본을 가리 킵니다. 그런 다음 기본 클래스는 기본 클래스 포인터를 통해이 새 객체에 액세스 Base*하고으로 래핑 하고 외부에서 호출되는 unique_ptr실제 clone()함수를 통해 반환 할 수 있습니다.


3
이것은 받아 들여진 대답이어야합니다. 다른 모든 사람들은이 스레드에서 원을 그리며, unique_ptr직접 격리가 그렇지 않을 때 가리키는 대상을 복사하려는 이유를 암시 하지 않습니다. 대답??? 상속 .
Tanveer Badar

4
다음과 같은 다양한 이유로 특정 유형이 가리키는 것을 알고있는 경우에도 unique_ptr을 사용할 수 있습니다. 1. nullable이어야합니다. 2. pointee는 매우 커서 스택 공간이 제한 될 수 있습니다. 종종 (1)과 (2)가 함께 사용되므로 경우에 따라 nullable 형식 unique_ptr보다 선호 할 수 optional있습니다.
Ponkadoodle

3
여드름 관용구는 또 다른 이유입니다.
emsr 2008

기본 클래스가 추상이 아니어야한다면? 순수 지정자없이 그대로두면 파생에서 다시 구현하는 것을 잊은 경우 런타임 버그가 발생할 수 있습니다.
Oleksij Plotnyc'kyj

1
@ OleksijPlotnyc'kyj : 예, clone_implin base 를 구현하면 컴파일러는 파생 클래스에서 잊어 버린 경우 알려주지 않습니다. 그러나 다른 기본 클래스를 사용 Cloneable하고 clone_impl거기에 순수 가상을 구현할 수 있습니다. 그런 다음 파생 클래스에서 잊어 버리면 컴파일러가 불평합니다.
davidhigh

11

이 도우미를 사용하여 깊은 복사본을 만들고 소스 unique_ptr이 null 일 때 대처하십시오.

    template< class T >
    std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
    {
        return source ? std::make_unique<T>(*source) : nullptr;
    }

예 :

class My
{
    My( const My& rhs )
        : member( copy_unique(rhs.member) )
    {
    }

    // ... other methods

private:
    std::unique_ptr<SomeType> member;
};

2
소스가 T에서 파생 된 것을 가리키면 올바르게 복사됩니까?
Roman Shapovalov

3
@RomanShapovalov 아니, 아마 아니에요. 이 경우 해결책은 가상 unique_ptr <T> clone () 메서드를 T 유형에 추가하고 T에서 파생 된 유형에 clone () 메서드의 재정의를 제공하는 것입니다. clone 메서드는의 새 인스턴스를 만듭니다. 파생 된 유형을 반환합니다.
Scott Langham

딥 복사 기능이 내장 된 C ++ 또는 부스트 라이브러리에 고유 / 범위가 지정된 포인터가 없습니까? 이러한 스마트 포인터를 사용하는 클래스에 대해 사용자 정의 복사 생성자 등을 만들 필요가없는 것이 좋을 것입니다. 종종 이러한 경우에 딥 복사 동작을 원할 때입니다. 그냥 궁금해.
shadow_map

5

복사 솔루션에 대한 Daniel Frey 언급, unique_ptr 이동 방법에 대해 이야기하겠습니다.

#include <memory>
class A
{
  public:
    A() : a_(new int(33)) {}

    A(A &&data) : a_(std::move(data.a_))
    {
    }

    A& operator=(A &&data)
    {
      a_ = std::move(data.a_);
      return *this;
    }

  private:
    std::unique_ptr<int> a_;
};

이동 생성자 및 이동 할당이라고합니다.

이런 식으로 사용할 수 있습니다

int main()
{
  A a;
  A b(std::move(a)); //this will call move constructor, transfer the resource of a to b

  A c;
  a = std::move(c); //this will call move assignment, transfer the resource of c to a

}

a와 c를 std :: move로 래핑해야합니다. std :: move는 매개 변수가 무엇이든간에 값을 rvalue 참조로 변환하도록 컴파일러에 지시합니다. 기술적 의미에서 std :: move는 " std :: rvalue "

이동 후 unique_ptr의 자원은 다른 unique_ptr로 이전됩니다.

rvalue 참조를 문서화하는 많은 주제가 있습니다. 이것은 시작하기 매우 쉬운 것입니다 .

편집하다 :

이동 된 객체 는 유효하지만 지정되지 않은 상태로 유지됩니다 .

C ++ 입문서 5, ch13은 객체를 "이동"하는 방법에 대한 아주 좋은 설명을 제공합니다.


1
그래서 이동 생성자 a에서 std :: move (a)를 호출 한 후 객체는 어떻게됩니까 b? 완전히 무효입니까?
David Doria

3

make_unique를 사용하는 것이 좋습니다.

class A
{
   std::unique_ptr< int > up_;

public:
   A( int i ) : up_(std::make_unique<int>(i)) {}
   A( const A& a ) : up_(std::make_unique<int>(*a.up_)) {};

int main()
{
   A a( 42 );
   A b = a;
}

-1

unique_ptr 복사 할 수 없으며 이동 만 가능합니다.

이것은 테스트에 직접적인 영향을 미치며, 두 번째 예제에서는 이동 만 가능하고 복사 할 수 없습니다.

사실 unique_ptr큰 실수로부터 당신을 보호하는 사용 하는 것이 좋습니다 .

예를 들어, 첫 번째 코드의 주요 문제는 포인터가 삭제되지 않는다는 것입니다. 다음과 같이이 문제를 해결할 수 있습니다.

class Test
{
    int* ptr; // writing this in one line is meh, not sure if even standard C++

    Test() : ptr(new int(10)) {}
    ~Test() {delete ptr;}
};

int main()
{       
     Test o;
     Test t = o;
}

이것도 나쁘다. 복사하면 Test어떻게 되나요? 동일한 주소를 가리키는 포인터가있는 두 개의 클래스가 있습니다.

하나 Test가 파괴 되면 포인터도 파괴됩니다. 두 번째 Test가 파괴되면 포인터 뒤의 메모리도 제거하려고 시도합니다. 그러나 이미 삭제되었으며 잘못된 메모리 액세스 런타임 오류 (또는 운이 좋지 않은 경우 정의되지 않은 동작)가 발생합니다.

따라서 올바른 방법은 복사 생성자와 복사 할당 연산자를 구현하여 동작이 명확하고 복사본을 만들 수 있도록하는 것입니다.

unique_ptr여기서 우리보다 훨씬 앞서 있습니다. 그것은 의미 론적 의미를 가지고 있습니다 : " I am unique, 그래서 당신은 나를 복사 할 수 없습니다. "그래서, 그것은 우리가 지금 당장 연산자를 구현하는 실수를 방지합니다.

특별한 동작을 위해 복사 생성자와 복사 할당 연산자를 정의 할 수 있으며 코드가 작동합니다. 그러나 당신은 당연히 그렇게 (!) 그렇게해야합니다.

이야기의 교훈 : 항상 unique_ptr이러한 종류의 상황에서 사용하십시오.

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