ฉันจะส่งผ่านอาร์กิวเมนต์ unique_ptr ไปยังตัวสร้างหรือฟังก์ชันได้อย่างไร


400

ฉันใหม่เพื่อย้ายซีแมนทิกส์ใน C ++ 11 และฉันไม่รู้วิธีจัดการunique_ptrพารามิเตอร์ใน Constructor หรือฟังก์ชั่น พิจารณาคลาสนี้อ้างอิง:

#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&&ที่มีการอ้างอิง nextBaseR-มูลค่าให้กับ นั่นคือทั้งหมดที่มันทำ

เนื่องจากBase::Base(std::unique_ptr<Base> n)รับอาร์กิวเมนต์เป็นค่าแทนที่จะอ้างอิงโดย r-value C ++ จะสร้างชั่วคราวสำหรับเราโดยอัตโนมัติ มันจะสร้างstd::unique_ptr<Base>จากที่เราให้ฟังก์ชั่นผ่านทางBase&& std::move(nextBase)มันคือการก่อสร้างชั่วคราวที่จริงย้ายค่าจากโต้เถียงฟังก์ชั่นnextBasen

(B) โดยการอ้างอิง l-value ที่ไม่ใช่ const

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

สิ่งนี้จะต้องถูกเรียกบนค่า l จริง (ตัวแปรที่มีชื่อ) มันไม่สามารถเรียกได้ว่าเป็นแบบชั่วคราว:

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

ความหมายของสิ่งนี้เป็นเช่นเดียวกับความหมายของการใช้งานการอ้างอิงที่ไม่ใช่แบบอื่น ๆ : ฟังก์ชั่นอาจหรือไม่อาจเรียกร้องความเป็นเจ้าของของตัวชี้ รับรหัสนี้:

Base newBase(nextBase);

ไม่มีการรับประกันที่nextBaseว่างเปล่า มันอาจจะเป็นที่ว่างเปล่า; มันอาจจะไม่ มันขึ้นอยู่กับสิ่งที่Base::Base(std::unique_ptr<Base> &n)ต้องการทำ ด้วยเหตุนี้มันจึงไม่ปรากฏชัดเพียงแค่จากลายเซ็นฟังก์ชันที่กำลังจะเกิดขึ้น คุณต้องอ่านการใช้งาน (หรือเอกสารที่เกี่ยวข้อง)

ด้วยเหตุนี้ฉันจึงไม่แนะนำสิ่งนี้เป็นส่วนต่อประสาน

(C) โดยการอ้างอิงค่า l const

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

ฉันจะไม่แสดงการดำเนินงานเพราะคุณไม่สามารถconst&ย้ายจาก โดยผ่าน a const&คุณกำลังบอกว่าฟังก์ชั่นสามารถเข้าถึงBaseผ่านตัวชี้ แต่มันไม่สามารถเก็บไว้ที่ใดก็ได้ ไม่สามารถอ้างสิทธิ์ความเป็นเจ้าของได้

สิ่งนี้มีประโยชน์ ไม่จำเป็นสำหรับกรณีเฉพาะของคุณ แต่เป็นเรื่องดีเสมอที่จะสามารถส่งตัวชี้ให้ใครบางคนและรู้ว่าพวกเขาไม่สามารถ (โดยไม่มีการละเมิดกฎของ C ++ เช่นไม่ทิ้งไปconst) อ้างสิทธิ์ความเป็นเจ้าของ พวกเขาไม่สามารถจัดเก็บได้ พวกเขาสามารถส่งต่อให้ผู้อื่นได้ แต่คนอื่นต้องปฏิบัติตามกฎเดียวกัน

(D) โดยการอ้างอิง r-value

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

นี่เป็นกรณีที่เหมือนกับ "โดยการอ้างอิง l-value 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 ลิตรมูลค่า:ถ้าคุณหมายถึงฟังก์ชั่นที่จะเพียงแค่ใช้ในช่วงระยะเวลาของการดำเนินการของฟังก์ชั่นที่นำโดยunique_ptr const&อีกวิธีหนึ่งคือผ่าน&หรือประเภทที่เกิดขึ้นจริงที่ชี้ไปมากกว่าใช้const&unique_ptr
  • (D) โดยอ้างอิง R-value:ถ้าฟังก์ชั่นอาจหรือไม่อาจเรียกร้องความเป็นเจ้าของ (ขึ้นอยู่กับเส้นทางรหัสภายใน) &&แล้วเอาไปโดย แต่ฉันขอแนะนำอย่างยิ่งให้ทำเช่นนี้ทุกครั้งที่ทำได้

วิธีจัดการกับ 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-value จริง ๆ แล้วนี่คือจุดประสงค์เพื่อการสาธิตเท่านั้น)

temporaryเป็นเพียงการอ้างอิง oldPtrR-มูลค่าให้กับ มันอยู่ในตัวสร้างของnewPtrการเคลื่อนไหวที่เกิดขึ้น unique_ptrตัวสร้างการย้าย (ตัวสร้างที่ใช้&&กับตัวเอง) คือการเคลื่อนไหวที่แท้จริง

หากคุณมีunique_ptrค่าและคุณต้องการเก็บไว้ที่ไหนสักแห่งคุณต้องใช้std::moveในการจัดเก็บ


5
@Nicol: แต่std::moveไม่ได้ตั้งชื่อค่าส่งคืน โปรดจำไว้ว่าการอ้างอิง rvalue ที่มีชื่อว่า lvalues ideone.com/VlEM3
R. Martinho Fernandes

31
ฉันเห็นด้วยกับคำตอบพื้นฐาน แต่มีข้อสังเกตบางอย่าง (1) ฉันไม่คิดว่ามีกรณีการใช้ที่ถูกต้องสำหรับการส่งผ่านการอ้างอิงไปยัง const lvalue: ทุกสิ่งที่ callee สามารถทำกับสิ่งนั้นได้ก็สามารถทำได้ด้วยการอ้างอิงถึงตัวชี้ const (เปลือย) เช่นกันหรือดีกว่าตัวชี้ [และ มันไม่มีธุรกิจในการเป็นเจ้าของรู้ที่จะจัดขึ้น throught unique_ptr; บางทีผู้โทรบางรายอาจต้องการฟังก์ชั่นที่เหมือนกัน แต่มีการเรียกใช้shared_ptrแทน] (2) การโทรโดยการอ้างอิง lvalue อาจเป็นประโยชน์หากฟังก์ชันที่เรียกว่าแก้ไขตัวชี้เช่นการเพิ่มหรือลบโหนด (รายการที่เป็นเจ้าของ) จากรายการที่เชื่อมโยง
Marc van Leeuwen

8
... (3) แม้ว่าการโต้แย้งของคุณจะชอบการส่งผ่านค่ามากกว่าการส่งผ่านโดยการอ้างอิง rvalue ก็สมเหตุสมผล แต่ฉันคิดว่ามาตรฐานนั้นมักจะส่งผ่านunique_ptrค่าโดยการอ้างอิง rvalue เสมอ(ตัวอย่างเช่นเมื่อเปลี่ยนเป็นshared_ptr) เหตุผลสำหรับการที่อาจเป็นไปได้ว่ามันเป็นเรื่องเล็กน้อยมีประสิทธิภาพมากขึ้น (ย้ายไปชี้ชั่วคราวไม่มีจะทำ) ในขณะที่มันจะช่วยให้สิทธิเช่นเดียวที่แน่นอนในการโทร (อาจจะผ่าน rvalues หรือ lvalues ห่อstd::moveแต่ไม่ lvalues เปลือยกาย)
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เทมเพลตคลาสที่เก่ากว่า(ซึ่งฉันเชื่อว่าอนุญาตให้ใช้ทั้งหมดที่ตัวชี้ที่ไม่ซ้ำกันทำ แต่ในนอกจากนี้ค่า lvalues ​​ที่ปรับเปลี่ยนได้จะได้รับการยอมรับเมื่อคาดว่าค่า rvalues ​​โดยไม่ต้องเรียกใช้std::move) และในระดับstd::shared_ptrหนึ่งด้วย

เป็นตัวอย่างที่เป็นรูปธรรมสำหรับการอภิปรายฉันจะพิจารณาประเภทรายการง่าย ๆ ดังต่อไปนี้

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

อินสแตนซ์ของรายการดังกล่าว (ซึ่งไม่สามารถได้รับอนุญาตให้แบ่งปันชิ้นส่วนกับอินสแตนซ์อื่นหรือเป็นแบบวงกลม) เป็นเจ้าของทั้งหมดโดยผู้ที่ถือlistตัวชี้เริ่มต้น ถ้ารหัสลูกค้ารู้ว่ารายการที่ร้านค้าจะไม่ว่างก็อาจเลือกที่จะเก็บครั้งแรกโดยตรงมากกว่าnode listไม่nodeจำเป็นต้องกำหนด destructor: เนื่องจาก destructors สำหรับเขตข้อมูลจะถูกเรียกโดยอัตโนมัติรายการทั้งหมดจะถูกลบซ้ำโดย destructor ตัวชี้อัจฉริยะเมื่ออายุการใช้งานของตัวชี้หรือโหนดเริ่มต้นสิ้นสุดลง

ประเภทเรียกซ้ำนี้ให้โอกาสในการหารือเกี่ยวกับบางกรณีที่มองเห็นได้น้อยลงในกรณีของตัวชี้สมาร์ทกับข้อมูลธรรมดา ฟังก์ชั่นเองก็มีตัวอย่างของรหัสลูกค้า (ซ้ำ) อีกด้วย typedef listของหลักสูตรมีอคติต่อunique_ptrแต่คำจำกัดความสามารถเปลี่ยนเป็นการใช้auto_ptrหรือshared_ptrแทนโดยไม่จำเป็นต้องเปลี่ยนสิ่งที่ได้กล่าวไว้ด้านล่าง

โหมดการผ่านตัวชี้อัจฉริยะรอบ ๆ

โหมด 0: ผ่านตัวชี้หรืออาร์กิวเมนต์อ้างอิงแทนตัวชี้อัจฉริยะ

หากฟังก์ชั่นของคุณไม่เกี่ยวข้องกับความเป็นเจ้าของนี่เป็นวิธีที่ต้องการ: อย่าทำให้มันใช้ตัวชี้สมาร์ทเลย ในกรณีนี้ฟังก์ชั่นของคุณไม่จำเป็นต้องกังวลว่าใครเป็นเจ้าของวัตถุที่ชี้ไปหรือด้วยการจัดการความเป็นเจ้าของดังนั้นการส่งตัวชี้แบบดิบจึงปลอดภัยอย่างสมบูรณ์และรูปแบบที่ยืดหยุ่นที่สุดเนื่องจากไม่ว่าลูกค้าจะเป็นเจ้าของได้ก็ตาม สร้างตัวชี้ raw (โดยการเรียกใช้getเมธอดหรือจาก address-of operator &)

เช่นฟังก์ชั่นในการคำนวณความยาวของรายการดังกล่าวไม่ควรให้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 nlength(&n)

หากตัวชี้มีการรับประกันว่าจะไม่เป็นโมฆะ (ซึ่งไม่ใช่ในกรณีนี้เนื่องจากรายการอาจว่างเปล่า) หนึ่งอาจต้องการผ่านการอ้างอิงมากกว่าตัวชี้ มันอาจจะเป็นตัวชี้ / อ้างอิงถึงไม่ใช่constถ้าฟังก์ชั่นจำเป็นต้องปรับปรุงเนื้อหาของโหนดโดยไม่ต้องเพิ่มหรือลบใด ๆ ของพวกเขา (หลังจะเกี่ยวข้องกับความเป็นเจ้าของ)

กรณีที่น่าสนใจที่อยู่ในหมวดหมู่โหมด 0 คือการทำสำเนา (ลึก) ของรายการ ในขณะที่ฟังก์ชั่นที่ทำสิ่งนี้จะต้องถ่ายโอนความเป็นเจ้าของสำเนาที่สร้างขึ้น แต่ไม่เกี่ยวข้องกับความเป็นเจ้าของรายการที่กำลังคัดลอก ดังนั้นจึงสามารถกำหนดได้ดังนี้

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

รหัสนี้มองใกล้ทั้งคำถามที่ว่าทำไมมันรวบรวมเลย (ผลของการเรียกซ้ำcopyในรายการเริ่มต้นผูกกับอาร์กิวเมนต์อ้างอิง rvalue ในตัวสร้างการย้ายของunique_ptr<node>อาคาlistเมื่อเริ่มต้นnextสนามของ สร้างnode) และสำหรับคำถามที่ว่าทำไมมันเป็นข้อยกเว้นปลอดภัย (หากในระหว่างหน่วยความจำขั้นตอนการจัดสรร recursive หมดและบางคนเรียกการnewพ่นstd::bad_allocแล้วในเวลานั้นตัวชี้ไปยังรายการที่สร้างส่วนหนึ่งที่จะจัดขึ้นในนามชั่วคราวประเภทlistสร้างขึ้นสำหรับรายการผู้เริ่มต้นและตัวทำลายมันจะล้างรายการบางส่วนนั้น) โดยวิธีการหนึ่งควรต่อต้านการทดลองที่จะเปลี่ยน (ตามที่ผมเริ่มได้) ครั้งที่สองnullptrโดยpซึ่งหลังจากทั้งหมดทราบว่าเป็นโมฆะ ณ จุดนั้น: หนึ่งไม่สามารถสร้างตัวชี้สมาร์ทจากตัวชี้ (ดิบ) ถึงค่าคงที่แม้ว่ามันจะเป็นโมฆะ

โหมด 1: ผ่านตัวชี้สมาร์ทตามค่า

ฟังก์ชั่นที่ใช้ค่าตัวชี้สมาร์ทเป็นข้อโต้แย้งครอบครองวัตถุที่ชี้ไปทันที: ตัวชี้สมาร์ทที่โทรเข้า (ไม่ว่าจะอยู่ในตัวแปรชื่อหรือชั่วคราวที่ไม่ระบุชื่อ) จะถูกคัดลอกลงในค่าอาร์กิวเมนต์ที่ทางเข้าฟังก์ชันและผู้โทร ตัวชี้กลายเป็นโมฆะ (ในกรณีของชั่วคราวสำเนาอาจถูกลบออกไป แต่ในกรณีใด ๆ ที่ผู้โทรได้สูญเสียการเข้าถึงไปยังวัตถุที่ชี้ไปที่) ฉันต้องการโทรหาโหมดนี้ด้วยเงินสด : ผู้โทรจ่ายเงินล่วงหน้าสำหรับบริการที่เรียกว่าและไม่สามารถมีภาพลวงตาเกี่ยวกับความเป็นเจ้าของหลังจากการโทร เพื่อให้ชัดเจนกฎภาษาจำเป็นต้องมีผู้โทรเพื่อตัดอาร์กิวเมนต์std::moveถ้าตัวชี้สมาร์ทถูกเก็บไว้ในตัวแปร (ในทางเทคนิคถ้าอาร์กิวเมนต์เป็น lvalue); ในกรณีนี้ (แต่ไม่ใช่สำหรับโหมด 3 ด้านล่าง) ฟังก์ชั่นนี้ทำในสิ่งที่ชื่อแนะนำคือย้ายค่าจากตัวแปรไปยังชั่วคราวโดยปล่อยตัวแปรว่างไว้

สำหรับกรณีที่ฟังก์ชั่นที่เรียกโดยไม่มีเงื่อนไขจะเป็นเจ้าของ (pilfers) วัตถุแบบชี้ไปที่โหมดนี้ใช้กับstd::unique_ptrหรือstd::auto_ptrเป็นวิธีที่ดีในการส่งตัวชี้พร้อมกับความเป็นเจ้าของซึ่งจะช่วยลดความเสี่ยงต่อการรั่วไหลของหน่วยความจำ อย่างไรก็ตามฉันคิดว่ามีเพียงไม่กี่สถานการณ์เท่านั้นที่ไม่ควรเลือกใช้โหมด 3 ด้านล่าง (เหนือกว่าเล็กน้อย) ในโหมด 1 ด้วยเหตุนี้ฉันจะไม่แสดงตัวอย่างการใช้งานของโหมดนี้ (แต่ดูreversedตัวอย่างของโหมด 3 ด้านล่างซึ่งมีการตั้งข้อสังเกตว่าโหมด 1 จะทำอย่างน้อยเช่นกัน) หากฟังก์ชันใช้อาร์กิวเมนต์มากกว่าตัวชี้นี้อาจเกิดขึ้นได้ว่ามีเหตุผลทางเทคนิคเพิ่มเติมเพื่อหลีกเลี่ยงโหมด 1 (พร้อมstd::unique_ptrหรือstd::auto_ptr): เนื่องจากการดำเนินการย้ายที่แท้จริงเกิดขึ้นขณะผ่านตัวแปรตัวชี้pโดยนิพจน์std::move(p)จะไม่สามารถสันนิษฐานได้ว่าpมีค่าที่มีประโยชน์ในขณะที่ประเมินอาร์กิวเมนต์อื่น ๆ (ลำดับของการประเมินผลไม่ได้ระบุ) ซึ่งอาจนำไปสู่ข้อผิดพลาดเล็กน้อย โดยคมชัดใช้โหมด 3 มั่นใจว่าจะไม่มีการย้ายจากpที่เกิดขึ้นก่อนที่จะเรียกฟังก์ชั่นเพื่อการขัดแย้งอื่น ๆ pสามารถเข้าถึงค่าผ่านทางได้อย่างปลอดภัย

เมื่อใช้กับstd::shared_ptrโหมดนี้มีความน่าสนใจด้วยการกำหนดฟังก์ชั่นเดียวที่ช่วยให้ผู้โทรเลือกว่าจะเก็บสำเนาการแชร์ของตัวชี้ไว้หรือไม่ในขณะที่สร้างสำเนาการแบ่งปันใหม่ที่จะใช้โดยฟังก์ชัน (เกิดขึ้นเมื่อ lvalue มีการจัดเตรียมตัวสร้างการคัดลอกสำหรับพอยน์เตอร์ที่ใช้ร่วมกันที่ใช้ในการโทรเพิ่มจำนวนการอ้างอิง) หรือเพียงแค่ให้ฟังก์ชั่นการคัดลอกของตัวชี้โดยไม่ต้องเก็บหนึ่งหรือสัมผัสนับอ้างอิง (เกิดขึ้นเมื่ออาร์กิวเมนต์ rvalue อาจเป็นไปได้ lvalue ห่อด้วยการเรียกstd::move) ตัวอย่างเช่น

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) แต่รุ่นที่สองย้าย semantics (เขียนstd::move(x)แทนเช่นในรหัสตัวอย่าง) ดังนั้นสำหรับพอยน์เตอร์ที่ใช้ร่วมกันโหมด 1 จึงมีประโยชน์ในการหลีกเลี่ยงการทำซ้ำโค้ด

โหมด 2: ผ่านตัวชี้สมาร์ทโดยอ้างอิง lvalue (แก้ไขได้)

ที่นี่ฟังก์ชั่นเพียงแค่ต้องมีการอ้างอิงแก้ไขได้กับตัวชี้สมาร์ท แต่ไม่ได้บ่งชี้ว่ามันจะทำอะไรกับมัน ฉันต้องการโทรหาวิธีนี้โทรด้วยบัตร : ผู้โทรยืนยันการชำระเงินโดยแจ้งหมายเลขบัตรเครดิต การอ้างอิงสามารถใช้ในการเป็นเจ้าของวัตถุชี้ไปที่ แต่ไม่จำเป็นต้อง โหมดนี้ต้องการการจัดเตรียมอาร์กิวเมนต์ lvalue ที่แก้ไขได้ซึ่งสอดคล้องกับความจริงที่ว่าผลที่ต้องการของฟังก์ชันอาจรวมถึงการทิ้งค่าที่มีประโยชน์ในตัวแปรอาร์กิวเมนต์ ผู้เรียกที่มีนิพจน์ rvalue ที่ต้องการส่งไปยังฟังก์ชันดังกล่าวจะถูกบังคับให้เก็บไว้ในตัวแปรที่กำหนดชื่อเพื่อให้สามารถโทรได้เนื่องจากภาษาให้การแปลงโดยนัยเป็นค่าคงที่เท่านั้นการอ้างอิง lvalue (อ้างอิงถึงชั่วคราว) จาก rvalue (ไม่เหมือนกับสถานการณ์ตรงกันข้ามที่จัดการโดยstd::moveไม่สามารถใช้การส่งจากY&&ไปยังY&พร้อมกับYตัวชี้แบบสมาร์ทอย่างไรก็ตามการแปลงนี้สามารถรับได้โดยฟังก์ชั่นเทมเพลตอย่างง่ายหากต้องการจริงๆดูhttps://stackoverflow.com/a/24868376 / 1436796 ) สำหรับกรณีที่ฟังก์ชั่นที่เรียกว่าตั้งใจที่จะเป็นเจ้าของวัตถุโดยไม่มีเงื่อนไขขโมยจากการโต้เถียงภาระหน้าที่ในการให้ข้อโต้แย้ง lvalue ให้สัญญาณผิด: ตัวแปรจะไม่มีค่าที่มีประโยชน์หลังจากการโทร ดังนั้นโหมด 3 ซึ่งให้ความเป็นไปได้ที่เหมือนกันในฟังก์ชั่นของเรา แต่ขอให้ผู้โทรแจ้งค่า rvalue ควรเป็นที่ต้องการสำหรับการใช้งานดังกล่าว

แต่มีกรณีที่ใช้ที่ถูกต้องสำหรับ 2 โหมดคือฟังก์ชั่นที่อาจแก้ไขตัวชี้หรือวัตถุที่ชี้ไปในทางที่เกี่ยวข้องกับการเป็นเจ้าของ ตัวอย่างเช่นฟังก์ชันที่นำหน้าโหนดไปยัง a 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จะคืนค่าตัวชี้ แต่สร้างโมฆะดั้งเดิม) ก่อน reset (โดยปริยาย) จะทำลายโหนดนั้น (เมื่อมันทำลายค่าเก่าที่ถือโดยp) หนึ่งและหนึ่งโหนดเท่านั้นที่ถูกทำลายในเวลานั้น (ในรูปแบบทางเลือกที่กล่าวถึงในความคิดเห็นช่วงเวลานี้จะถูกปล่อยให้อยู่ใน internals ของการดำเนินการของผู้ประกอบการย้ายที่ได้รับมอบหมายของstd::unique_ptrอินสแตนซ์listมาตรฐานพูดว่า 20.7.1.2.3; 2 ว่าผู้ประกอบการนี้ควรกระทำ "ราวกับว่า เรียกreset(u.release())"ดังนั้นเวลาควรปลอดภัยที่นี่ด้วย)

โปรดทราบว่าprependและremove_firstไม่สามารถเรียกใช้โดยลูกค้าที่เก็บnodeตัวแปรท้องถิ่นสำหรับรายการที่ไม่ว่างเสมอและถูกต้องดังนั้นเนื่องจากการใช้งานที่ให้ไว้ไม่สามารถใช้งานได้สำหรับกรณีดังกล่าว

โหมด 3: ผ่านตัวชี้สมาร์ทโดยอ้างอิงค่า rvalue (แก้ไขได้)

นี่คือโหมดที่ต้องการใช้เมื่อเพียงแค่เป็นเจ้าของพอยน์เตอร์ ฉันต้องการเรียกวิธีการนี้โดยใช้เช็ค : ผู้โทรต้องยอมรับการเป็นเจ้าของราวกับให้เงินสดโดยเซ็นชื่อในเช็ค แต่การถอนเงินจริงจะถูกเลื่อนออกไปจนกว่าฟังก์ชั่นที่เรียกจะใช้ตัวชี้ (ตามที่ใช้โหมด 2 ) "การลงนามในเช็ค" เป็นรูปธรรมหมายถึงผู้โทรต้องตัดอาร์กิวเมนต์ในstd::move(เช่นในโหมด 1) หากเป็น lvalue (หากเป็น 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));การย้อนกลับรายการเป็นของตัวเอง แต่รายการกลับรายการสามารถใช้ที่แตกต่างกัน

ที่นี่อาร์กิวเมนต์ถูกย้ายไปยังตัวแปรโลคัลทันทีเพื่อประสิทธิภาพ (หนึ่งสามารถใช้พารามิเตอร์lโดยตรงในสถานที่pแต่จากนั้นการเข้าถึงมันในแต่ละครั้งจะเกี่ยวข้องกับระดับพิเศษของการอ้อม); ดังนั้นความแตกต่างกับการส่งผ่านอาร์กิวเมนต์โหมด 1 จึงน้อยที่สุด ในความเป็นจริงโดยใช้โหมดนั้นอาร์กิวเมนต์อาจทำหน้าที่เป็นตัวแปรท้องถิ่นโดยตรงดังนั้นจึงหลีกเลี่ยงการย้ายครั้งแรก นี่เป็นเพียงตัวอย่างของหลักการทั่วไปที่ถ้าอาร์กิวเมนต์ที่ส่งผ่านโดยการอ้างอิงทำหน้าที่เริ่มต้นตัวแปรท้องถิ่นเพียงอย่างเดียวอาจส่งผ่านค่านั้นโดยใช้ค่าแทนและใช้พารามิเตอร์เป็นตัวแปรท้องถิ่น

การใช้โหมด 3 ดูเหมือนจะสนับสนุนโดยมาตรฐานเท่าที่เห็นจากข้อเท็จจริงที่ว่าฟังก์ชั่นทั้งหมดให้ห้องสมุดที่โอนกรรมสิทธิ์ของตัวชี้สมาร์ทใช้โหมด 3. std::shared_ptr<T>(auto_ptr<T>&& p)กรณีพิเศษเชื่อในจุดคือสร้าง คอนสตรัคเตอร์นั้นใช้ (ใน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 ที่นี่ แต่พวกเขามีตัวเลือกในการเปลี่ยนเป็นโหมด 1 โดยกำหนดstd::shared_ptr<T>(auto_ptr<T> p)แต่พวกเขาสามารถมั่นใจได้ว่าโค้ดเก่าทำงานโดยไม่มีการดัดแปลงเพราะ (ไม่เหมือนกับพอยน์เตอร์ที่ไม่ซ้ำกัน) พอยน์เตอร์อัตโนมัติสามารถทำการตรวจสอบค่าได้อย่างเงียบ ๆ (วัตถุตัวชี้จะถูกรีเซ็ตเป็น null ในกระบวนการ เห็นได้ชัดว่าคณะกรรมการที่ต้องการให้การสนับสนุนโหมด 3 มากกว่าโหมด 1 พวกเขาเลือกที่จะทำลายรหัสที่มีอยู่อย่างแข็งขันมากกว่าที่จะใช้โหมด 1 แม้สำหรับการใช้งานที่เลิกใช้แล้ว

เมื่อใดที่ต้องการโหมด 3 มากกว่าโหมด 1

โหมด 1 สามารถใช้งานได้อย่างสมบูรณ์แบบในหลายกรณีและอาจได้รับความนิยมมากกว่าโหมด 3 ในกรณีที่สมมติว่าความเป็นเจ้าของจะใช้รูปแบบของการย้ายตัวชี้สมาร์ทไปยังตัวแปรท้องถิ่นตามreversedตัวอย่างด้านบน อย่างไรก็ตามฉันเห็นเหตุผลสองประการที่ต้องการโหมด 3 ในกรณีทั่วไปมากขึ้น:

  • มันมีประสิทธิภาพมากกว่าเล็กน้อยในการผ่านการอ้างอิงกว่าการสร้างชั่วคราวและระวังตัวชี้เก่า (การจัดการเงินสดค่อนข้างลำบาก) ในบางสถานการณ์ตัวชี้อาจถูกส่งผ่านหลายครั้งไม่เปลี่ยนแปลงไปยังฟังก์ชั่นอื่นก่อนที่จะถูกขโมยจริง การส่งผ่านดังกล่าวโดยทั่วไปจะต้องมีการเขียนstd::move(เว้นแต่ใช้โหมด 2) แต่โปรดทราบว่านี่เป็นเพียงนักแสดงที่ไม่ได้ทำอะไรเลย

  • ควรเป็นไปได้หรือไม่ว่าสิ่งใดจะทำให้เกิดข้อยกเว้นระหว่างจุดเริ่มต้นของการเรียกฟังก์ชันและจุดที่ (หรือบางสายที่มีอยู่) เคลื่อนย้ายวัตถุที่ชี้ไปยังโครงสร้างข้อมูลอื่น (และข้อยกเว้นนี้ไม่ได้ติดอยู่ภายในตัวฟังก์ชันเอง ) จากนั้นเมื่อใช้โหมด 1 วัตถุที่อ้างถึงโดยตัวชี้สมาร์ทจะถูกทำลายก่อนที่catchข้อสามารถจัดการข้อยกเว้น (เพราะพารามิเตอร์ฟังก์ชั่นถูกทำลายในระหว่างการคลี่คลายคลี่คลาย) แต่ไม่ได้ดังนั้นเมื่อใช้โหมด 3 ผู้เรียกมีตัวเลือกในการกู้คืนข้อมูลของวัตถุในกรณีดังกล่าว (โดยการตรวจจับข้อยกเว้น) โปรดทราบว่าโหมด 1 ที่นี่ไม่ทำให้หน่วยความจำรั่วแต่อาจนำไปสู่การสูญเสียข้อมูลที่ไม่สามารถกู้คืนได้สำหรับโปรแกรมซึ่งอาจไม่เป็นที่ต้องการเช่นกัน

กลับมาเป็นตัวชี้สมาร์ท: ตามค่าเสมอ

เพื่อสรุปคำศัพท์เกี่ยวกับการคืนค่าพอยน์เตอร์สมาร์ทให้ชี้ไปที่วัตถุที่สร้างขึ้นเพื่อใช้โดยผู้โทร นี่ไม่ใช่กรณีที่เทียบได้กับการส่งพอยน์เตอร์ไปยังฟังก์ชั่น แต่เพื่อความสมบูรณ์ฉันขอยืนยันว่าในกรณีเช่นนี้จะคืนค่าเสมอ (และไม่ได้ใช้ std::moveในreturnคำสั่ง) ไม่มีใครต้องการได้รับการอ้างอิงไปยังตัวชี้ที่อาจเพิ่งได้รับการผสม


1
+1 สำหรับโหมด 0 - ผ่านตัวชี้พื้นฐานแทนที่จะเป็น unique_ptr ปิดหัวข้อเล็กน้อย (เนื่องจากคำถามเกี่ยวกับการส่งผ่าน unique_ptr) แต่มันง่ายและหลีกเลี่ยงปัญหา
Machta

" โหมด 1 ที่นี่ไม่ทำให้หน่วยความจำรั่ว " - ซึ่งบอกเป็นนัยว่าโหมด 3 ทำให้หน่วยความจำรั่วซึ่งไม่เป็นความจริง ไม่ว่าจะunique_ptrถูกย้ายจากหรือไม่ก็ตามจะยังคงเป็นการลบค่าหากยังคงมีอยู่เมื่อใดก็ตามที่ถูกทำลายหรือนำกลับมาใช้ใหม่
rustyx

@RustyX: ฉันไม่สามารถเห็นว่าคุณตีความความหมายนั้นอย่างไรและฉันไม่เคยตั้งใจจะพูดในสิ่งที่คุณคิดว่าฉันบอกเป็นนัย ทั้งหมดที่ฉันหมายถึงคือที่อื่น ๆ การใช้งานของการunique_ptrป้องกันการรั่วไหลของหน่วยความจำ (และในความหมายตามสัญญา) แต่ที่นี่ (เช่นการใช้โหมด 1) มันอาจทำให้เกิด (ภายใต้สถานการณ์ที่เฉพาะเจาะจง) บางสิ่งบางอย่าง คือการสูญเสียข้อมูล (การทำลายของค่าชี้ไป) ที่สามารถหลีกเลี่ยงได้ใช้โหมด 3
มาร์คฟานลีเวน

4

ใช่คุณต้องถ้าคุณใช้unique_ptrโดยค่าในตัวสร้าง ความชัดเจนเป็นสิ่งที่ดี เนื่องจากunique_ptrไม่มีการคัดลอก (ctor สำเนาส่วนตัว) สิ่งที่คุณเขียนควรให้ข้อผิดพลาดของคอมไพเลอร์


3

แก้ไข:คำตอบนี้ผิดแม้ว่าการพูดอย่างเคร่งครัดรหัสทำงาน ฉันแค่ทิ้งไว้ที่นี่เพราะการสนทนาภายใต้มันมีประโยชน์มากเกินไป คำตอบอื่น ๆ นี้เป็นคำตอบที่ดีที่สุดที่ได้รับในเวลาที่ฉันแก้ไขครั้งสุดท้ายนี้: ฉันจะส่งผ่านอาร์กิวเมนต์ unique_ptr ไปยังตัวสร้างหรือฟังก์ชันได้อย่างไร

แนวคิดพื้นฐานของการ::std::moveที่คนที่ผ่านคุณunique_ptrควรใช้มันเพื่อแสดงความรู้ที่พวกเขารู้ว่าunique_ptrพวกเขากำลังผ่านจะสูญเสียกรรมสิทธิ์

ซึ่งหมายความว่าคุณควรใช้การอ้างอิง rvalue กับunique_ptrวิธีการของคุณไม่ใช่unique_ptrตัวของมันเอง นี้จะไม่ทำงานแล้วเพราะผ่านในเก่าธรรมดาจะต้องมีการทำสำเนาและที่ต้องห้ามอย่างชัดเจนในอินเตอร์เฟซสำหรับ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 ที่ระบุชื่อคือ lvalues
R. Martinho Fernandes

คุณแน่ใจหรือว่าBase fred(::std::move(objptr));ไม่ใช่และBase::UPtr fred(::std::move(objptr));?
codablank1

1
เพื่อเพิ่มความคิดเห็นก่อนหน้าของฉัน: รหัสนี้จะไม่รวบรวม คุณยังจำเป็นต้องใช้std::moveในการใช้งานทั้งตัวสร้างและวิธีการ และแม้กระทั่งเมื่อคุณผ่านค่าผู้โทรยังต้องใช้std::moveเพื่อผ่านค่า lvalues ข้อแตกต่างที่สำคัญคือเมื่อใช้การส่งผ่านค่าที่อินเตอร์เฟสทำให้การเป็นเจ้าของชัดเจนจะหายไป ดูความคิดเห็นของ Nicol Bolas ในคำตอบอื่น
R. Martinho Fernandes

@ codablank1: ใช่ ฉันแสดงให้เห็นถึงวิธีการใช้ตัวสร้างและวิธีการในฐานที่ใช้การอ้างอิงค่า
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)

ด้วยร่างกายเดียวกัน

และ ... สิ่งที่อยู่evtในhandle()??


3
ไม่มีประโยชน์ในการใช้std::forwardที่นี่: Base::UPtr&&เป็นประเภทอ้างอิง rvalue เสมอและstd::moveส่งผ่านเป็น rvalue มันถูกส่งต่อไปแล้วอย่างถูกต้อง
R. Martinho Fernandes

7
ฉันไม่เห็นด้วยอย่างยิ่ง หากฟังก์ชั่นใช้unique_ptrค่าโดยคุณจะรับประกันได้ว่าตัวสร้างการย้ายถูกเรียกกับค่าใหม่ (หรือเพียงแค่ว่าคุณได้รับชั่วคราว) นี้เพื่อให้แน่ใจว่าunique_ptrตัวแปรที่ผู้ใช้มีอยู่ในขณะนี้ที่ว่างเปล่า หากคุณดำเนินการ&&แทนจะเป็นการลบข้อมูลหากรหัสของคุณเรียกใช้การย้าย วิธีของคุณเป็นไปได้สำหรับตัวแปรที่ผู้ใช้ไม่ได้ถูกย้ายจาก ทำให้ผู้ใช้std::moveต้องสงสัยและสับสน การใช้std::moveควรให้แน่ใจว่าสิ่งที่ถูกย้าย
Nicol Bolas

@NicolBolas: คุณพูดถูก ฉันจะลบคำตอบของฉันเพราะในขณะที่ทำงานการสังเกตของคุณถูกต้องอย่างแน่นอน
Omnifarious

0

ไปที่คำตอบที่ได้รับการโหวตสูงสุด ฉันชอบที่จะผ่านการอ้างอิงค่า

ฉันเข้าใจว่าปัญหาเกี่ยวกับการส่งผ่านการอ้างอิงค่าอาจทำให้เกิดอะไรขึ้น แต่ลองแบ่งปัญหานี้เป็นสองด้าน:

  • สำหรับผู้โทร:

ผมต้องเขียนโค้ดหรือBase newBase(std::move(<lvalue>))Base newBase(<rvalue>)

  • สำหรับ callee:

ผู้เขียนไลบรารีควรรับประกันว่าจริง ๆ แล้วมันจะย้าย unique_ptr เพื่อเริ่มต้นสมาชิกหากต้องการเป็นเจ้าของ

นั่นคือทั้งหมดที่

ถ้าคุณผ่านการอ้างอิง rvalue มันจะเรียกเพียงหนึ่งคำสั่ง "ย้าย" แต่ถ้าผ่านตามค่ามันเป็นสอง

ใช่ถ้าผู้เขียนห้องสมุดไม่เชี่ยวชาญเกี่ยวกับเรื่องนี้เขาอาจไม่ย้าย unique_ptr เพื่อเริ่มต้นสมาชิก แต่เป็นปัญหาของผู้เขียนไม่ใช่คุณ ไม่ว่าจะผ่านการอ้างอิงค่าหรือการประเมินค่าใดรหัสของคุณเหมือนกัน!

หากคุณกำลังเขียนห้องสมุดตอนนี้คุณรู้ว่าคุณควรรับประกันมันดังนั้นเพียงทำมันผ่านการอ้างอิง rvalue เป็นตัวเลือกที่ดีกว่าค่า ลูกค้าที่ใช้ห้องสมุดของคุณจะเขียนรหัสเดียวกัน

ตอนนี้สำหรับคำถามของคุณ ฉันจะส่งผ่านอาร์กิวเมนต์ 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.