ส่งคืน unique_ptr จากฟังก์ชั่น


367

unique_ptr<T>ไม่อนุญาตการสร้างสำเนา แต่จะรองรับซีแมนทิกส์ของการย้าย แต่ฉันสามารถคืนค่าunique_ptr<T>จากฟังก์ชันและกำหนดค่าที่ส่งคืนให้กับตัวแปรได้

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

รหัสข้างต้นรวบรวมและทำงานตามที่ตั้งใจไว้ ดังนั้นบรรทัด1นั้นจะไม่เรียกใช้ตัวสร้างการคัดลอกและทำให้เกิดข้อผิดพลาดของคอมไพเลอร์อย่างไร ถ้าฉันต้องใช้ไลน์2แทนมันก็สมเหตุสมผลดี (การใช้2งานไลน์ก็เช่นกัน แต่เราไม่จำเป็นต้องทำเช่นนั้น)

ฉันรู้ว่า C ++ 0x อนุญาตให้มีข้อยกเว้นนี้unique_ptrเนื่องจากค่าส่งคืนเป็นวัตถุชั่วคราวที่จะถูกทำลายทันทีที่ออกจากฟังก์ชั่นดังนั้นจึงรับประกันความเป็นเอกลักษณ์ของตัวชี้ที่ส่งคืน ฉันอยากรู้เกี่ยวกับวิธีการใช้งานนี้มันเป็นกรณีพิเศษในคอมไพเลอร์หรือมีข้ออื่น ๆ ในข้อกำหนดภาษาที่หาประโยชน์ได้หรือไม่


สมมุติฐานถ้าคุณใช้วิธีการจากโรงงานคุณต้องการให้ 1 หรือ 2 เพื่อส่งคืนผลผลิตของโรงงานหรือไม่ ฉันคิดว่านี่จะเป็นการใช้งานที่บ่อยที่สุด 1 เนื่องจากมีโรงงานที่เหมาะสมคุณต้องการให้เจ้าของสิ่งที่สร้างขึ้นส่งผ่านไปยังผู้โทรจริงๆ
Xharlie

7
@Xharlie? unique_ptrพวกเขาทั้งสองผ่านการเป็นเจ้าของ คำถามทั้งหมดเกี่ยวกับ 1 และ 2 เป็นสองวิธีที่แตกต่างกันในการบรรลุสิ่งเดียวกัน
Praetorian

ในกรณีนี้ RVO จะเกิดขึ้นใน c ++ 0x เช่นกันการทำลายวัตถุ unique_ptr จะดำเนินการทันทีหลังจากที่mainออกจากฟังก์ชัน แต่ไม่ใช่เมื่อfooออก
ampawd

คำตอบ:


218

มีข้ออื่น ๆ ในข้อกำหนดภาษาที่หาประโยชน์ได้หรือไม่

ใช่ดู 12.8 §34และ§35:

เมื่อตรงตามเกณฑ์ที่กำหนดจะอนุญาตให้ละเว้นการสร้างสำเนา / ย้ายของคลาสอ็อบเจ็กต์ [... ] การดำเนินการคัดลอก / ย้ายนี้เรียกว่าการคัดลอกการคัดลอกได้รับอนุญาต [... ] ในคำสั่งส่งคืนใน ฟังก์ชั่นที่มีประเภทการคืนคลาสเมื่อนิพจน์เป็นชื่อของวัตถุอัตโนมัติที่ไม่ลบเลือนด้วยประเภท CV-unqualified เดียวกับประเภทการคืนค่าฟังก์ชัน [... ]

เมื่อเกณฑ์สำหรับการตัดออกจากการดำเนินการคัดลอกจะได้พบและวัตถุที่จะคัดลอกถูกกำหนดโดย lvalue ละเอียดเกินเพื่อเลือกสร้างสำหรับการคัดลอกจะดำเนินการเป็นครั้งแรกเช่นถ้าวัตถุที่ถูกกำหนดโดย rvalue


แค่ต้องการเพิ่มอีกหนึ่งจุดที่การคืนค่าควรเป็นตัวเลือกเริ่มต้นที่นี่เนื่องจากค่าที่ระบุชื่อในคำสั่ง return ในกรณีที่เลวร้ายที่สุดคือไม่มีการใช้ตัวอักษรใน C ++ 11, C ++ 14 และ C ++ 17 เป็นค่า rvalue ตัวอย่างเช่นฟังก์ชั่นต่อไปนี้คอมไพล์ด้วย-fno-elide-constructorsแฟล็ก

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

ด้วยการตั้งค่าสถานะในการรวบรวมมีสองย้าย (1 และ 2) เกิดขึ้นในฟังก์ชั่นนี้แล้วหนึ่งย้ายในภายหลังใน (3)


@juanchopanza จริงๆแล้วคุณหมายถึงว่าfoo()กำลังจะถูกทำลาย (ถ้าไม่ได้ถูกมอบหมายให้ทำอะไร) เช่นเดียวกับค่าส่งคืนภายในฟังก์ชันและด้วยเหตุนี้มันจึงสมเหตุสมผลที่ C ++ ใช้ตัวสร้างการย้ายเมื่อทำเช่นนั้นunique_ptr<int> p = foo();?
7cows

1
คำตอบนี้บอกว่าการนำไปปฏิบัติได้รับอนุญาตให้ทำบางสิ่งบางอย่าง ... มันไม่ได้บอกว่าต้องทำดังนั้นหากนี่เป็นส่วนที่เกี่ยวข้องเพียงอย่างเดียวนั่นก็หมายถึงการพึ่งพาพฤติกรรมนี้ไม่ใช่พกพา แต่ฉันไม่คิดว่าถูกต้อง ฉันมีแนวโน้มที่จะคิดว่าคำตอบที่ถูกต้องเกี่ยวข้องกับตัวสร้างการย้ายมากขึ้นดังที่อธิบายไว้ในคำตอบของ Nikola Smiljanic และ Bartosz Milewski
Don Hatch

6
@ DonHatch มันบอกว่ามันเป็น "อนุญาต" เพื่อทำการคัดลอก / ย้าย elision ในกรณีเหล่านั้น แต่เราไม่ได้พูดถึงการคัดลอกที่นี่ มันเป็นย่อหน้าที่สองที่ยกมาซึ่งใช้ที่นี่ซึ่ง piggy-backs เกี่ยวกับกฎการคัดลอกการคัดลอก แต่ไม่ใช่การคัดลอกการคัดลอกตัวเอง ไม่มีความไม่แน่นอนในย่อหน้าที่สอง - มันพกพาได้โดยสิ้นเชิง
Joseph Mansfield

@juanchopanza ฉันรู้ว่าตอนนี้ 2 ปีต่อมา แต่คุณยังรู้สึกว่ามันผิดหรือเปล่า? ดังที่ฉันพูดถึงในความคิดเห็นก่อนหน้านี้ไม่เกี่ยวกับการคัดลอก มันเกิดขึ้นอย่างนั้นในกรณีที่อาจใช้การคัดลอกข้อมูล (แม้ว่าจะใช้ไม่ได้std::unique_ptr) ก็มีกฎพิเศษในการจัดการวัตถุเป็นค่าแรก ฉันคิดว่าสิ่งนี้เห็นด้วยกับสิ่งที่ Nikola ตอบ
Joseph Mansfield

1
เหตุใดฉันจึงยังคงได้รับข้อผิดพลาด "พยายามอ้างอิงฟังก์ชันที่ถูกลบ" สำหรับประเภทย้ายอย่างเดียวของฉัน (ตัวสร้างสำเนาที่ลบออก) เมื่อส่งกลับในลักษณะเดียวกับตัวอย่างนี้
DrumM

104

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

หากคุณมีฟังก์ชั่นที่ยอมรับstd::unique_ptrว่าเป็นข้อโต้แย้งคุณจะไม่สามารถส่งผ่าน p ไปได้ คุณจะต้องชัดเจนเรียก constructor ย้าย bar()แต่ในกรณีนี้คุณไม่ควรใช้ตัวแปรพีหลังจากที่โทรไป

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

3
@ เฟร็ด - ดีไม่จริง แม้ว่าpจะไม่ใช่ชั่วคราว แต่ผลลัพธ์ของfoo()สิ่งที่ส่งคืนคือ ดังนั้นจึงเป็นค่า rvalue และสามารถเคลื่อนย้ายได้ซึ่งทำให้การกำหนดmainเป็นไปได้ ฉันว่าคุณผิดยกเว้น Nikola นั้นก็ดูเหมือนว่าจะใช้กฎนี้กับpตัวเองซึ่งเป็นข้อผิดพลาด
Edward Strange

สิ่งที่ฉันอยากจะพูด แต่ก็หาคำไม่เจอ ฉันได้ลบคำตอบส่วนนั้นออกเนื่องจากไม่ชัดเจน
Nikola Smiljanić

ฉันมีคำถาม: ในคำถามเดิมมีความแตกต่างอย่างมากระหว่าง Line 1กับ Line 2หรือไม่? ในมุมมองของฉันมันเหมือนกันตั้งแต่เมื่อสร้างpในmainก็กังวลเกี่ยวกับประเภทของประเภทการกลับมาของfooใช่มั้ย?
Hongxu Chen

1
@HongxuChen ในตัวอย่างนั้นไม่มีความแตกต่างอย่างเห็นได้จากราคามาตรฐานในคำตอบที่ยอมรับ
Nikola Smiljanić

ที่จริงแล้วคุณสามารถใช้ p หลังจากนั้นตราบใดที่คุณกำหนดให้มัน ก่อนหน้านี้คุณไม่สามารถอ้างอิงเนื้อหาได้
Alan

38

unique_ptr ไม่มีตัวสร้างสำเนาแบบดั้งเดิม แต่มี "ตัวสร้างการย้าย" ที่ใช้การอ้างอิง rvalue แทน:

unique_ptr::unique_ptr(unique_ptr && src);

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

โดยวิธีการนี้จะทำงานอย่างถูกต้อง:

bar(unique_ptr<int>(new int(44));

unique_ptr ชั่วคราวที่นี่คือค่าใช้จ่าย


8
ผมคิดว่าจุดที่มากขึ้นทำไมp- "เห็นได้ชัดว่า" lvalue - ได้รับการปฏิบัติในฐานะที่เป็นrvalueในงบการกลับมาในความหมายของreturn p; fooฉันไม่คิดว่าจะมีปัญหาใด ๆ กับความจริงที่ว่าค่าส่งคืนของฟังก์ชันสามารถ "ย้าย" ได้
CB Bailey

การตัดค่าที่ส่งคืนจากฟังก์ชันใน std :: move หมายความว่ามันจะถูกย้ายสองครั้งหรือไม่?

3
@RodrigoSalazar std :: move เป็นเพียงนักแสดงแฟนซีจากการอ้างอิง lvalue (&) ถึงการอ้างอิง rvalue (&&) การใช้งานภายนอกของ std :: ย้ายการอ้างอิงค่า rvalue จะเป็นแบบ noop
TiMoch

13

ฉันคิดว่ามันอธิบายอย่างสมบูรณ์แบบในรายการที่ 25ของสกอตต์เมเยอร์สที่มีประสิทธิภาพสมัยใหม่ C นี่คือข้อความที่ตัดตอนมา:

ส่วนหนึ่งของพรมาตรฐานที่ RVO กล่าวต่อไปว่าหากเงื่อนไขสำหรับ RVO ตรงกัน แต่คอมไพเลอร์เลือกที่จะไม่ทำการคัดลอกคัดลอกวัตถุที่ถูกส่งคืนจะต้องได้รับการปฏิบัติในฐานะที่เป็นค่า rvalue มาตรฐานจะกำหนดว่าเมื่อได้รับอนุญาต RVO จะมีการคัดลอกเกิดขึ้นหรือstd::moveนำไปใช้กับวัตถุในท้องถิ่นที่ส่งคืนโดยปริยาย

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

ในกรณีของคุณรหัสนั้นจะเป็นตัวเลือกที่ชัดเจนสำหรับRVOเนื่องจากมันจะส่งคืนวัตถุในท้องถิ่นpและประเภทของpจะเหมือนกับชนิดส่งคืนซึ่งส่งผลให้เกิดการคัดลอก และถ้า Chooses คอมไพเลอร์จะไม่มองข้ามการคัดลอกด้วยเหตุผลใดจะได้เตะในกับสายstd::move1


5

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

ตัวอย่างอาจเป็นดังนี้:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

มันถูกกล่าวถึงในคำตอบโดย fredoverflow - เน้น " วัตถุอัตโนมัติ " อย่างชัดเจน การอ้างอิง (รวมถึงการอ้างอิง rvalue) ไม่ใช่วัตถุอัตโนมัติ
Toby Speight

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