วิธี“ คืนวัตถุ” ใน C ++ ได้อย่างไร


167

ฉันรู้ว่าชื่อฟังดูคุ้นหูเพราะมีคำถามคล้าย ๆ กันหลายอย่าง แต่ฉันขอปัญหาที่แตกต่าง (ฉันรู้ถึงความแตกต่างระหว่างการมีสิ่งของในกองซ้อน

ใน Java ฉันสามารถกลับไปอ้างอิงถึงวัตถุ "ท้องถิ่น"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

ใน C ++ เพื่อทำสิ่งที่คล้ายกันฉันมี 2 ตัวเลือก

(1) ฉันสามารถใช้การอ้างอิงเมื่อใดก็ตามที่ฉันต้องการ "ส่งคืน" วัตถุ

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

จากนั้นใช้มันเช่นนี้

Thing thing;
calculateThing(thing);

(2) หรือฉันสามารถคืนค่าตัวชี้ไปยังวัตถุที่ถูกจัดสรรแบบไดนามิก

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

จากนั้นใช้มันเช่นนี้

Thing* thing = calculateThing();
delete thing;

การใช้วิธีแรกฉันจะไม่ต้องเพิ่มหน่วยความจำด้วยตนเอง แต่สำหรับฉันมันทำให้โค้ดอ่านยาก ปัญหาเกี่ยวกับแนวทางที่สองคือฉันต้องจำไว้delete thing;ซึ่งไม่ได้ดูดีนัก ฉันไม่ต้องการคืนค่าที่คัดลอกเพราะไม่มีประสิทธิภาพ (ฉันคิดว่า) ดังนั้นคำถามมาที่นี่

  • มีวิธีที่สาม (ไม่ต้องการคัดลอกค่า) หรือไม่
  • มีปัญหาใดไหมถ้าฉันติดกับวิธีแก้ปัญหาแรก?
  • ฉันควรใช้โซลูชันที่สองเมื่อใดและเพราะเหตุใด

32
+1 สำหรับการตั้งคำถาม
Kangkan

1
ถ้าจะพูดจาหยาบคายมันเป็นเรื่องไม่แน่ที่จะพูดว่า "ฟังก์ชั่นคืนอะไรบางอย่าง" อื่น ๆ ได้อย่างถูกต้องประเมินสายงานการผลิตที่คุ้มค่า ค่านั้นเป็นวัตถุเสมอ (เว้นแต่ว่าเป็นฟังก์ชัน void) ความแตกต่างไม่ว่าจะเป็นค่าที่เป็น glvalue หรือ prvalue - ซึ่งจะถูกกำหนดโดยว่าผลตอบแทนประเภทประกาศเป็นอ้างอิงหรือไม่
Kerrek SB

คำตอบ:


107

ฉันไม่ต้องการส่งคืนค่าที่คัดลอกเนื่องจากไม่มีประสิทธิภาพ

พิสูจน์สิ.

ค้นหา RVO และ NRVO และใน C ++ 0x move-semantics ในกรณีส่วนใหญ่ใน C ++ 03 พารามิเตอร์ out เป็นวิธีที่ดีในการทำให้โค้ดของคุณน่าเกลียดและใน C ++ 0x คุณต้องทำร้ายตัวเองด้วยการใช้พารามิเตอร์ out

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


ที่กล่าวว่าหากคุณไม่ได้ตั้งใจเขียนเช่นนั้นคุณอาจต้องการทำพารามิเตอร์ out หลีกเลี่ยงการจัดสรรหน่วยความจำแบบไดนามิกซึ่งปลอดภัยและเร็วกว่า มันต้องการให้คุณมีวิธีในการสร้างวัตถุก่อนที่จะเรียกใช้ฟังก์ชันซึ่งอาจไม่เหมาะสมสำหรับวัตถุทั้งหมด

หากคุณต้องการใช้การจัดสรรแบบไดนามิกอย่างน้อยที่สุดที่สามารถทำได้คือวางไว้ในตัวชี้สมาร์ท (สิ่งนี้ควรทำตลอดเวลาอยู่แล้ว) จากนั้นคุณไม่ต้องกังวลเกี่ยวกับการลบสิ่งต่าง ๆ สิ่งต่าง ๆ ปลอดภัยยกเว้น ฯลฯ ปัญหาเดียวคือมันน่าจะช้ากว่าคืนค่า!


10
@phunehehe: ไม่มีการคาดเดาจุดคุณควรใส่รหัสของคุณและค้นหา (คำแนะนำ: ไม่) คอมไพเลอร์ฉลาดมากพวกเขาจะไม่เสียเวลาในการคัดลอกสิ่งต่าง ๆ หากไม่จำเป็น แม้ว่าการคัดลอกมีค่าใช้จ่ายบางอย่างคุณก็ควรที่จะพยายามให้รหัสที่ดีอยู่เหนือรหัสที่รวดเร็ว รหัสที่ดีนั้นง่ายต่อการเพิ่มประสิทธิภาพเมื่อความเร็วกลายเป็นปัญหา ไม่มีจุดในการอัปเดตโค้ดที่น่าเกลียดสำหรับสิ่งที่คุณไม่มีความคิดว่าเป็นปัญหา โดยเฉพาะอย่างยิ่งถ้าคุณทำให้มันช้าลงหรือไม่ได้อะไรเลย และถ้าคุณใช้ C ++ 0x การย้ายความหมายทำให้นี่ไม่ใช่ประเด็น
GManNickG

1
@GMan, อีกครั้ง: RVO: จริง ๆ แล้วนี่เป็นเรื่องจริงหากผู้โทรและผู้เรียกของคุณอยู่ในหน่วยการรวบรวมเดียวกันซึ่งในโลกแห่งความเป็นจริงมันไม่ใช่เวลาส่วนใหญ่ ดังนั้นคุณจะผิดหวังถ้ารหัสของคุณไม่ใช่เทมเพลตทั้งหมด (ในกรณีนี้มันจะอยู่ในหน่วยรวบรวม) หรือคุณมีการเพิ่มประสิทธิภาพลิงค์เวลา (GCC มีเพียง 4.5)
Alex B

2
@Alex: คอมไพเลอร์เริ่มดีขึ้นและดีขึ้นในการเพิ่มประสิทธิภาพของหน่วยการแปล (VC ทำเพื่อเผยแพร่หลายตอน)
sbi

9
@Alex B: นี่คือขยะสมบูรณ์ การประชุมทางโทรศัพท์ที่ใช้กันทั่วไปจำนวนมากทำให้ผู้โทรรับผิดชอบในการจัดสรรพื้นที่สำหรับค่าส่งคืนที่มีขนาดใหญ่และผู้รับสายที่รับผิดชอบในการก่อสร้าง RVO ทำงานอย่างมีความสุขในหน่วยการรวบรวมแม้ไม่มีการเพิ่มประสิทธิภาพเวลาเชื่อม
CB Bailey

6
@Charles เมื่อตรวจสอบแล้วดูเหมือนถูกต้อง! ฉันถอนคำสั่งที่เข้าใจผิดอย่างชัดเจน
Alex B

41

เพียงแค่สร้างวัตถุและคืนมัน

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

ฉันคิดว่าคุณจะทำสิ่งที่ดีหากคุณลืมเกี่ยวกับการปรับให้เหมาะสมและเพียงแค่เขียนโค้ดที่สามารถอ่านได้ (คุณจะต้องเรียกใช้ profiler ในภายหลัง - แต่ไม่ต้องปรับแต่งล่วงหน้า)


2
Thing thing();Thingประกาศฟังก์ชั่นในท้องถิ่นและกลับ
Dreamlax

2
สิ่ง () ประกาศฟังก์ชั่นกลับสิ่ง ไม่มีวัตถุสิ่งที่สร้างขึ้นในร่างกายของคุณทำงาน
CB Bailey

@dreamlax @Charles @GMan สายไปหน่อย แต่ได้รับการแก้ไข
Amir Rachum

สิ่งนี้ทำงานใน C ++ 98 ได้อย่างไร ฉันพบข้อผิดพลาดในล่าม CINT และสงสัยว่าเป็นเพราะ C ++ 98 หรือ CINT เอง ... !
xcorat

16

เพียงแค่คืนค่าวัตถุเช่นนี้:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

สิ่งนี้จะเรียกใช้ตัวสร้างการคัดลอกในสิ่งต่าง ๆ ดังนั้นคุณอาจต้องการดำเนินการสิ่งนั้นเอง แบบนี้:

Thing(const Thing& aThing) {}

สิ่งนี้อาจทำงานช้าลงเล็กน้อย แต่อาจไม่เป็นปัญหาเลย

ปรับปรุง

คอมไพเลอร์อาจจะปรับการเรียกไปยังตัวสร้างการคัดลอกดังนั้นจึงไม่มีค่าใช้จ่ายเพิ่มเติม (เหมือน Dreamlax ชี้ให้เห็นในความคิดเห็น)


9
Thing thing();ประกาศฟังก์ชั่นท้องถิ่นกลับมาThingนอกจากนี้มาตรฐานอนุญาตให้คอมไพเลอร์ที่จะละเว้นการสร้างสำเนาในกรณีที่คุณนำเสนอ; คอมไพเลอร์สมัยใหม่อาจทำเช่นนั้น
Dreamlax

1
คุณนำมาซึ่งจุดที่ดีโดยการใช้ตัวสร้างสำเนาโดยเฉพาะอย่างยิ่งหากจำเป็นต้องมีสำเนาลึก
mbadawi23

+1 สำหรับการระบุอย่างชัดเจนเกี่ยวกับตัวสร้างการคัดลอกแม้ว่าในขณะที่ @dreamlax บอกว่าคอมไพเลอร์จะ "ปรับ" โค้ดส่งคืนสำหรับฟังก์ชันที่หลีกเลี่ยงการเรียกการสร้างสำเนาที่ไม่จำเป็นจริงๆ
jose.angel.jimenez

ในปี 2018 ใน VS 2017 พยายามใช้ตัวสร้างการย้าย หากตัวสร้างการย้ายถูกลบและตัวสร้างการคัดลอกไม่ได้จะไม่รวบรวม
แอนดรู

11

คุณลองใช้พอยน์เตอร์อัจฉริยะ (ถ้าเป็นสิ่งที่มีขนาดใหญ่และหนักมาก) เช่น auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs เลิกใช้แล้ว ใช้shared_ptrหรือunique_ptrแทน
MBraedley

เพิ่งจะเพิ่มสิ่งนี้ในที่นี่ ... ฉันใช้ c ++ มาหลายปีแม้ว่าจะไม่ใช่มืออาชีพกับ c ++ ก็ตาม ... ฉันตัดสินใจแล้วว่าจะไม่ใช้ตัวชี้สมาร์ทอีกต่อไปพวกเขาเป็นเพียง imo ที่ยุ่งเหยิงและเป็นสาเหตุทั้งหมด ปัญหาต่าง ๆ ไม่ได้ช่วยเร่งความเร็วโค้ดมากนัก ฉันอยากจะคัดลอกข้อมูลไปรอบ ๆ และจัดการพอยน์เตอร์ของตัวเองโดยใช้ RAII ดังนั้นฉันแนะนำว่าถ้าทำได้ให้หลีกเลี่ยงตัวชี้ที่ฉลาด
แอนดรู

8

วิธีหนึ่งที่รวดเร็วในการพิจารณาว่าตัวสร้างสำเนากำลังถูกเรียกหรือไม่คือการเพิ่มการบันทึกลงในตัวสร้างสำเนาของคลาสของคุณ:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

โทรsomeFunction; จำนวนของ "ตัวสร้างการคัดลอกเรียกว่า" บรรทัดที่คุณจะได้รับจะแตกต่างกันระหว่าง 0, 1 และ 2 หากคุณไม่ได้รับแล้วคอมไพเลอร์ของคุณได้เพิ่มประสิทธิภาพการคืนค่าออก (ซึ่งได้รับอนุญาตให้ทำ) ถ้าคุณได้รับไม่ได้รับ 0 และตัวสร้างสำเนาของคุณมีราคาแพงขันแล้วค้นหาวิธีทางเลือกที่จะกลับมาจากอินสแตนซ์ฟังก์ชั่นของคุณ


1

ประการแรกคุณมีข้อผิดพลาดในรหัสคุณหมายถึงการที่จะมีและมีเพียงThing *thing(new Thing());return thing;

  • shared_ptr<Thing>ใช้ ลองอีกครั้งเพราะมันเป็นตัวชี้ มันจะถูกลบสำหรับคุณเมื่อการอ้างอิงล่าสุดของสิ่งที่Thingมีอยู่นอกขอบเขต
  • วิธีแก้ปัญหาแรกนั้นพบได้บ่อยในห้องสมุดไร้เดียงสา มันมีประสิทธิภาพบางอย่างและค่าใช้จ่ายในการสร้างประโยคหลีกเลี่ยงถ้าเป็นไปได้
  • ใช้วิธีแก้ไขปัญหาที่สองเฉพาะในกรณีที่คุณสามารถรับประกันว่าจะไม่มีข้อยกเว้นหรือเมื่อประสิทธิภาพการทำงานมีความสำคัญอย่างยิ่ง (คุณจะเชื่อมต่อกับ C หรือแอสเซมบลีก่อนหน้านี้แม้จะเกี่ยวข้อง)

0

ฉันแน่ใจว่าผู้เชี่ยวชาญ C ++ จะมาพร้อมกับคำตอบที่ดีกว่า แต่โดยส่วนตัวแล้วฉันชอบแนวทางที่สอง การใช้สมาร์ทพอยน์เตอร์ช่วยในเรื่องของการลืมdeleteและอย่างที่คุณพูดมันดูสะอาดกว่าการสร้างวัตถุก่อนมือ (และยังคงต้องลบมันถ้าคุณต้องการจัดสรรมันบนกอง)

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