คัดลอกตัวสร้างสำหรับคลาสที่มี unique_ptr


107

ฉันจะใช้ตัวสร้างการคัดลอกสำหรับคลาสที่มีunique_ptrตัวแปรสมาชิกได้อย่างไร ฉันกำลังพิจารณาเฉพาะ C ++ 11


9
คุณต้องการให้ตัวสร้างการคัดลอกทำอะไร
Nicol Bolas

ฉันอ่านว่า unique_ptr ไม่สามารถคัดลอกได้ สิ่งนี้ทำให้ฉันสงสัยว่าจะใช้คลาสที่มีตัวแปรสมาชิก unique_ptr ใน a std::vector.
codefx

2
@AbhijitKadam คุณสามารถทำสำเนาเนื้อหาของ unique_ptr ได้อย่างลึกซึ้ง อันที่จริงมักเป็นสิ่งที่ควรทำ
คิวบิก

2
โปรดทราบว่าคุณอาจถามคำถามผิด คุณอาจไม่ต้องการตัวสร้างการคัดลอกสำหรับชั้นเรียนของคุณที่มี a unique_ptrคุณอาจต้องการตัวสร้างการย้ายหากเป้าหมายของคุณคือการใส่ข้อมูลในไฟล์std::vector. ในทางกลับกันมาตรฐาน C ++ 11 ได้สร้างตัวสร้างการเคลื่อนไหวโดยอัตโนมัติดังนั้นคุณอาจต้องการตัวสร้างสำเนา ...
Yakk - Adam Nevraumont

3
องค์ประกอบเวกเตอร์ @codefx ไม่จำเป็นต้องคัดลอก นั่นหมายความว่าเวกเตอร์นั้นจะไม่สามารถคัดลอกได้
MM

คำตอบ:


82

เนื่องจากunique_ptrไม่สามารถแชร์ได้คุณจำเป็นต้องคัดลอกเนื้อหาในรายละเอียดหรือแปลงunique_ptrเป็น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 กล่าวถึงใช้ move-ctor แทน copy-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,
}

หากคุณต้องการใช้คลาสของคุณใน a std::vectorโดยพื้นฐานแล้วคุณต้องตัดสินใจว่าเวกเตอร์นั้นจะเป็นเจ้าของเฉพาะของอ็อบเจ็กต์หรือไม่ซึ่งในกรณีนี้จะเพียงพอที่จะทำให้คลาสเคลื่อนที่ได้ แต่ไม่สามารถคัดลอกได้ หากคุณละทิ้งการคัดลอก ctor และการกำหนดสำเนาคอมไพเลอร์จะแนะนำวิธีการใช้ std :: vector กับประเภทการย้ายอย่างเดียว


4
อาจคุ้มค่าที่จะกล่าวถึงผู้ก่อสร้างการย้าย?
NPE

4
+1 แต่ควรเน้นตัวสร้างการเคลื่อนไหวให้มากยิ่งขึ้น ในความคิดเห็น OP กล่าวว่าเป้าหมายคือการใช้วัตถุในเวกเตอร์ ด้วยเหตุนี้การย้ายการก่อสร้างและการย้ายการมอบหมายจึงเป็นสิ่งเดียวที่จำเป็น
jogojapan

36
intเป็นคำเตือนกลยุทธ์ข้างต้นทำงานชนิดง่ายๆเช่น หากคุณมีunique_ptr<Base>ที่เก็บ a Derivedด้านบนจะหั่น
Yakk - Adam Nevraumont

5
ไม่มีการตรวจสอบค่าว่างดังนั้นสิ่งนี้จึงอนุญาตให้ dereference nullptr How aboutA( const A& a ) : up_( a.up_ ? new int( *a.up_ ) : nullptr) {}
Ryan Haining

1
@Aaron ในสถานการณ์หลายรูปแบบตัวลบจะถูกลบอย่างใดอย่างหนึ่งหรือไม่มีจุดหมาย (ถ้าคุณรู้ประเภทที่จะลบทำไมต้องเปลี่ยนเฉพาะตัวลบ) ไม่ว่าในกรณีใด ๆ ใช่นี่คือการออกแบบข้อมูล a value_ptr- unique_ptrplus deleter / copier
Yakk - Adam Nevraumont

48

กรณีปกติสำหรับคนที่มี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เติมด้วยสำเนาของพอยน์ทีเดิมและใช้เป็นสมาชิกของคลาสที่คัดลอก

  • ในกรณีที่เกี่ยวข้องกับการรับมรดกต้องทำสำเนาของพอยน์เตอร์ตัวจริงอย่างรอบคอบ เหตุผลก็คือการทำสำเนาอย่างง่ายผ่านstd::unique_ptr<Base>(*ptr)โค้ดด้านบนจะส่งผลให้เกิดการแบ่งส่วนกล่าวคือเฉพาะส่วนประกอบพื้นฐานของวัตถุเท่านั้นที่ถูกคัดลอกในขณะที่ส่วนที่ได้รับหายไป

    เพื่อหลีกเลี่ยงปัญหานี้ต้องทำสำเนาผ่านรูปแบบการโคลน แนวคิดคือการทำสำเนาผ่านฟังก์ชันเสมือนclone_impl()ซึ่งส่งคืน a Base*ในคลาสฐาน อย่างไรก็ตามในคลาสที่ได้รับมันถูกขยายผ่านความแปรปรวนร่วมเพื่อส่งคืน a Derived*และตัวชี้นี้ชี้ไปที่สำเนาที่สร้างขึ้นใหม่ของคลาสที่ได้รับ จากนั้นคลาสฐานสามารถเข้าถึงอ็อบเจ็กต์ใหม่นี้ผ่านตัวชี้คลาสฐานBase*รวมเข้ากับ a unique_ptrและส่งคืนผ่านclone()ฟังก์ชันจริงซึ่งถูกเรียกใช้จากภายนอก


3
นี่ควรเป็นคำตอบที่ได้รับการยอมรับ คนอื่น ๆ ต่างก็เข้ามาเป็นวงกลมในชุดข้อความนี้โดยไม่ได้บอกใบ้ว่าเหตุใดจึงต้องการคัดลอกวัตถุที่ชี้ไปunique_ptrเมื่อการกักกันโดยตรงจะทำอย่างอื่น คำตอบ??? มรดก
Tanveer Badar

4
หนึ่งอาจใช้ unique_ptr แม้ว่าพวกเขาจะรู้ว่าชนิดของคอนกรีตถูกชี้ไปด้วยเหตุผลหลายประการ: 1. จำเป็นต้องเป็นโมฆะ 2. พอยน์ตี้มีขนาดใหญ่มากและเราอาจมีพื้นที่ จำกัด บ่อยครั้งที่ (1) และ (2) จะไปด้วยกันดังนั้นบางครั้งอาจชอบunique_ptrมากกว่าoptionalสำหรับประเภทที่เป็นโมฆะ
Ponkadoodle

3
สำนวนสิวเป็นอีกสาเหตุหนึ่ง
emsr

จะเกิดอะไรขึ้นถ้าคลาสพื้นฐานไม่ควรเป็นนามธรรม? การปล่อยทิ้งไว้โดยไม่มีตัวระบุที่แท้จริงอาจทำให้เกิดข้อบกพร่องในเวลาทำงานได้หากคุณลืมนำไปใช้ใหม่ในที่ได้รับ
olek stolar

1
@ OleksijPlotnyc'kyj: ใช่ถ้าคุณใช้clone_implin base คอมไพลเลอร์จะไม่บอกคุณว่าคุณลืมมันในคลาสที่ได้รับหรือไม่ อย่างไรก็ตามคุณสามารถใช้คลาสพื้นฐานอื่นCloneableและใช้งานเสมือนจริงได้ที่clone_implนั่น จากนั้นคอมไพเลอร์จะบ่นหากคุณลืมมันในคลาสที่ได้รับ
davidhigh

11

ลองใช้ตัวช่วยนี้เพื่อสร้างสำเนาแบบลึกและรับมือเมื่อแหล่งที่มา unique_ptr เป็นโมฆะ

    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 ไม่คงไม่คุณจะหั่น ในกรณีนั้นวิธีแก้ปัญหาน่าจะเป็นการเพิ่มเมธอด virtual unique_ptr <T> clone () ให้กับประเภท T ของคุณและจัดเตรียมการแทนที่ของ clone () วิธีการในประเภทที่ได้มาจาก T วิธีการโคลนจะสร้างอินสแตนซ์ใหม่ของ ประเภทที่ได้รับและส่งคืนนั้น
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 reference ไม่ว่าพารามิเตอร์จะเป็นอะไรก็ตามในทางเทคนิค std :: move นั้นคล้ายคลึงกับบางสิ่งเช่น " มาตรฐาน :: 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อยู่ข้างหน้าเราที่นี่ มันมีความหมายเชิงความหมาย: " ฉันเป็นuniqueดังนั้นคุณไม่สามารถคัดลอกฉันได้ " ดังนั้นจึงป้องกันไม่ให้เราผิดพลาดในการใช้ตัวดำเนินการที่อยู่ในมือ

คุณสามารถกำหนดตัวสร้างการคัดลอกและตัวดำเนินการกำหนดสำเนาสำหรับพฤติกรรมพิเศษและรหัสของคุณจะทำงาน แต่คุณถูกต้อง (!) ถูกบังคับให้ทำเช่นนั้น

คุณธรรมของเรื่องราว: ใช้unique_ptrในสถานการณ์แบบนี้เสมอ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.