Copy constructor และ = operator overload ใน C ++: เป็นฟังก์ชันทั่วไปที่เป็นไปได้หรือไม่?


87

ตั้งแต่ตัวสร้างสำเนา

MyClass(const MyClass&);

และตัวดำเนินการ = โอเวอร์โหลด

MyClass& operator = (const MyClass&);

มีรหัสเดียวกันพารามิเตอร์เดียวกันและแตกต่างกันเพียงแค่ผลตอบแทนเป็นไปได้ไหมที่จะมีฟังก์ชันทั่วไปให้ทั้งคู่ใช้?


6
"... มีรหัสเดียวกัน ... "? อืม ... คุณคงทำอะไรผิด พยายามลดความจำเป็นในการใช้ฟังก์ชันที่ผู้ใช้กำหนดเองสำหรับสิ่งนี้และปล่อยให้คอมไพเลอร์ทำงานสกปรกทั้งหมด ซึ่งมักหมายถึงการห่อหุ้มทรัพยากรในออบเจ็กต์สมาชิกของตนเอง คุณสามารถแสดงรหัสให้เราดู บางทีเราอาจมีข้อเสนอแนะในการออกแบบที่ดี
sellibitze

คำตอบ:


123

ใช่. มีสองตัวเลือกทั่วไป สิ่งหนึ่งซึ่งโดยทั่วไปไม่สนับสนุน - คือการเรียกตัวoperator=สร้างการคัดลอกอย่างชัดเจน:

MyClass(const MyClass& other)
{
    operator=(other);
}

อย่างไรก็ตามการให้สิ่งที่ดีoperator=เป็นสิ่งที่ท้าทายเมื่อต้องจัดการกับสภาพเก่าและปัญหาที่เกิดจากการมอบหมายงานด้วยตนเอง นอกจากนี้สมาชิกและฐานทั้งหมดจะได้รับการกำหนดค่าเริ่มต้นเป็นค่าเริ่มต้นก่อนแม้ว่าจะถูกกำหนดให้จากotherก็ตาม สิ่งนี้อาจไม่ถูกต้องสำหรับสมาชิกและฐานทั้งหมดและแม้ว่าจะถูกต้องก็ตาม แต่ก็มีความหมายซ้ำซ้อนและอาจมีราคาแพงในทางปฏิบัติ

วิธีแก้ปัญหาที่ได้รับความนิยมมากขึ้นคือการoperator=ใช้งานโดยใช้ตัวสร้างการคัดลอกและวิธีการแลกเปลี่ยน

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

หรือแม้กระทั่ง:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

swapฟังก์ชั่นโดยทั่วไปจะง่ายต่อการเขียนเป็นเพียงแค่แลกเปลี่ยนกรรมสิทธิ์ของ internals และไม่ได้มีการทำความสะอาดของรัฐที่มีอยู่หรือการจัดสรรทรัพยากรใหม่

ข้อดีของสำนวนการคัดลอกและการแลกเปลี่ยนคือการกำหนดเองโดยอัตโนมัติอย่างปลอดภัยและ - หากการดำเนินการแลกเปลี่ยนไม่มีการโยน - ยังปลอดภัยอย่างยิ่ง

เพื่อให้ปลอดภัยเป็นอย่างยิ่งผู้ดำเนินการมอบหมายงานที่เขียนด้วยมือโดยทั่วไปจะต้องจัดสรรสำเนาของทรัพยากรใหม่ก่อนที่จะยกเลิกการจัดสรรทรัพยากรเก่าของผู้รับมอบหมายดังนั้นหากมีข้อยกเว้นเกิดขึ้นในการจัดสรรทรัพยากรใหม่สถานะเก่าจะยังคงถูกส่งกลับไป . ทั้งหมดนี้มาพร้อมกับการ copy-and-swap ฟรี แต่โดยทั่วไปแล้วจะซับซ้อนกว่าและด้วยเหตุนี้จึงเกิดข้อผิดพลาดได้ง่ายตั้งแต่เริ่มต้น

สิ่งหนึ่งที่ต้องระวังคือตรวจสอบให้แน่ใจว่าวิธีการแลกเปลี่ยนเป็นการแลกเปลี่ยนที่แท้จริงไม่ใช่ค่าเริ่มต้นstd::swapที่ใช้ตัวสร้างการคัดลอกและตัวดำเนินการกำหนดเอง

โดยปกติ memberwise swapถูกนำมาใช้ std::swapใช้งานได้และรับประกัน 'ไม่โยน' ด้วยประเภทพื้นฐานและประเภทตัวชี้ทั้งหมด พอยน์เตอร์อัจฉริยะส่วนใหญ่สามารถสลับได้ด้วยการรับประกันไม่มีการโยน


3
จริงๆแล้วพวกเขาไม่ใช่การดำเนินการทั่วไป ในขณะที่ copy ctor เริ่มต้นสมาชิกของวัตถุเป็นครั้งแรกตัวดำเนินการกำหนดจะแทนที่ค่าที่มีอยู่ เมื่อพิจารณาถึงสิ่งนี้การอ้างอิงoperator=จาก ctor ที่คัดลอกนั้นค่อนข้างแย่มากเพราะก่อนอื่นจะเริ่มต้นค่าทั้งหมดเป็นค่าเริ่มต้นเพียงเพื่อแทนที่ค่าเหล่านั้นด้วยค่าของวัตถุอื่นในภายหลัง
sbi

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

4
พอใช้ฉันโหวตให้คุณแล้ว :-) ฉันคิดว่าหากมีการพิจารณาแนวทางปฏิบัติที่ดีที่สุดอย่างกว้างขวางแล้วก็ควรพูดเช่นนั้น (และดูอีกครั้งหากมีคนบอกว่ามันไม่ดีที่สุดจริงๆ) ในทำนองเดียวกันถ้ามีคนถามว่า "เป็นไปได้ไหมที่จะใช้ mutexes ใน C ++" ฉันจะไม่พูดว่า "ทางเลือกหนึ่งที่ใช้กันทั่วไปคือการละเว้น RAII โดยสิ้นเชิงและเขียนโค้ดที่ไม่ปลอดภัยซึ่งทำให้การหยุดชะงักในการผลิต แต่เป็นที่นิยมมากขึ้นในการเขียน รหัสที่ใช้งานได้ดี ";-)
Steve Jessop

4
+1. และฉันคิดว่าจำเป็นต้องมีการวิเคราะห์เสมอ ฉันคิดว่ามันสมเหตุสมผลที่จะมีassignฟังก์ชั่นสมาชิกที่ใช้โดยทั้ง copy ctor และตัวดำเนินการกำหนดในบางกรณี (สำหรับคลาสที่มีน้ำหนักเบา) ในกรณีอื่น ๆ (กรณีที่ใช้ทรัพยากรมาก / ใช้จัดการ / เนื้อหา) การคัดลอก / การแลกเปลี่ยนเป็นวิธีที่แน่นอน
Johannes Schaub - litb

2
@litb: ฉันรู้สึกประหลาดใจกับสิ่งนี้ดังนั้นฉันจึงค้นหารายการ 41 ในข้อยกเว้น C ++ (ซึ่งสิ่งนี้กลายเป็น) และคำแนะนำนี้ได้หายไปและเขาแนะนำให้คัดลอกและแลกเปลี่ยนแทน เขาได้ทิ้ง "ปัญหา # 4: ไม่มีประสิทธิภาพสำหรับการมอบหมาย" ในเวลาเดียวกันอย่างลับๆ
CB Bailey

14

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

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

สิ่งที่มักถือว่าเป็นสำนวนที่เป็นที่ยอมรับในปัจจุบันใช้swapตามที่ Charles แนะนำ:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

สิ่งนี้ใช้การสร้างสำเนา (หมายเหตุที่otherคัดลอก) และการทำลาย (ถูกทำลายเมื่อสิ้นสุดฟังก์ชัน) - และใช้ตามลำดับที่ถูกต้องเช่นกัน: การก่อสร้าง (อาจล้มเหลว) ก่อนการทำลาย (ต้องไม่ล้มเหลว)


ควรswapจะประกาศvirtual?

1
@ Johannes: ฟังก์ชันเสมือนถูกใช้ในลำดับชั้นของโพลีมอร์ฟิก ตัวดำเนินการกำหนดใช้สำหรับชนิดค่า ทั้งสองแทบจะไม่ผสมกัน
sbi

-3

มีบางอย่างรบกวนฉันเกี่ยวกับ:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

ขั้นแรกการอ่านคำว่า "swap" เมื่อใจของฉันคิดว่า "copy" ทำให้สามัญสำนึกของฉันระคายเคือง นอกจากนี้ฉันยังตั้งคำถามถึงเป้าหมายของเคล็ดลับแฟนซีนี้ ใช่ข้อยกเว้นใด ๆ ในการสร้างทรัพยากร (ที่คัดลอก) ใหม่ควรเกิดขึ้นก่อนการแลกเปลี่ยนซึ่งดูเหมือนเป็นวิธีที่ปลอดภัยในการตรวจสอบให้แน่ใจว่าได้กรอกข้อมูลใหม่ทั้งหมดก่อนที่จะเผยแพร่จริง

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

ดังนั้นฉันขอเสนอแทนที่จะ "สลับ" เพื่อทำการ "ถ่ายโอน" ที่เป็นธรรมชาติมากขึ้น:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

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

แทนที่จะเป็น {สร้างย้ายทำลาย} ฉันเสนอ {สร้างทำลายย้าย} การเคลื่อนไหวซึ่งเป็นการกระทำที่อันตรายที่สุดเป็นการกระทำครั้งสุดท้ายหลังจากที่ทุกอย่างถูกตัดสิน

ใช่ความล้มเหลวในการทำลายล้างเป็นปัญหาในทั้งสองโครงการ ข้อมูลอาจเสียหาย (คัดลอกโดยที่คุณไม่คิดว่าเป็นข้อมูล) หรือสูญหาย (เป็นอิสระเมื่อคุณไม่คิดว่าเป็นข้อมูล) หายดีกว่าเสียหาย ไม่มีข้อมูลใดดีไปกว่าข้อมูลที่ไม่ดี

โอนแทนการแลกเปลี่ยน นั่นเป็นคำแนะนำของฉันอยู่แล้ว


2
ผู้ทำลายจะต้องไม่ล้มเหลวดังนั้นจึงไม่คาดว่าจะมีข้อยกเว้นในการทำลายล้าง และฉันไม่ได้รับประโยชน์อะไรจากการเคลื่อนย้ายที่อยู่เบื้องหลังการทำลายล้างถ้าการเคลื่อนไหวเป็นการปฏิบัติการที่อันตรายที่สุด? กล่าวคือในรูปแบบมาตรฐานความล้มเหลวในการย้ายจะไม่ทำให้สถานะเก่าเสียหายในขณะที่โครงการใหม่ของคุณทำ แล้วทำไมล่ะ? นอกจากนี้First, reading the word "swap" when my mind is thinking "copy" irritates-> เป็นนักเขียนห้องสมุดคุณจะรู้ว่าการปฏิบัติทั่วไป (คัดลอกแลกเปลี่ยน +) my mindและปมคือ ความคิดของคุณซ่อนอยู่หลังอินเทอร์เฟซสาธารณะ นั่นคือสิ่งที่เกี่ยวกับโค้ดที่ใช้ซ้ำได้
Sebastian Mach
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.