ตั้งแต่ตัวสร้างสำเนา
MyClass(const MyClass&);
และตัวดำเนินการ = โอเวอร์โหลด
MyClass& operator = (const MyClass&);
มีรหัสเดียวกันพารามิเตอร์เดียวกันและแตกต่างกันเพียงแค่ผลตอบแทนเป็นไปได้ไหมที่จะมีฟังก์ชันทั่วไปให้ทั้งคู่ใช้?
ตั้งแต่ตัวสร้างสำเนา
MyClass(const MyClass&);
และตัวดำเนินการ = โอเวอร์โหลด
MyClass& operator = (const MyClass&);
มีรหัสเดียวกันพารามิเตอร์เดียวกันและแตกต่างกันเพียงแค่ผลตอบแทนเป็นไปได้ไหมที่จะมีฟังก์ชันทั่วไปให้ทั้งคู่ใช้?
คำตอบ:
ใช่. มีสองตัวเลือกทั่วไป สิ่งหนึ่งซึ่งโดยทั่วไปไม่สนับสนุน - คือการเรียกตัว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
ใช้งานได้และรับประกัน 'ไม่โยน' ด้วยประเภทพื้นฐานและประเภทตัวชี้ทั้งหมด พอยน์เตอร์อัจฉริยะส่วนใหญ่สามารถสลับได้ด้วยการรับประกันไม่มีการโยน
operator=
จาก ctor ที่คัดลอกนั้นค่อนข้างแย่มากเพราะก่อนอื่นจะเริ่มต้นค่าทั้งหมดเป็นค่าเริ่มต้นเพียงเพื่อแทนที่ค่าเหล่านั้นด้วยค่าของวัตถุอื่นในภายหลัง
assign
ฟังก์ชั่นสมาชิกที่ใช้โดยทั้ง copy ctor และตัวดำเนินการกำหนดในบางกรณี (สำหรับคลาสที่มีน้ำหนักเบา) ในกรณีอื่น ๆ (กรณีที่ใช้ทรัพยากรมาก / ใช้จัดการ / เนื้อหา) การคัดลอก / การแลกเปลี่ยนเป็นวิธีที่แน่นอน
ตัวสร้างการคัดลอกดำเนินการเริ่มต้นอ็อบเจ็กต์ที่เคยเป็นหน่วยความจำดิบเป็นครั้งแรก ตัวดำเนินการกำหนด OTOH จะแทนที่ค่าที่มีอยู่ด้วยค่าใหม่ บ่อยกว่าไม่เคยสิ่งนี้เกี่ยวข้องกับการยกเลิกทรัพยากรเก่า (เช่นหน่วยความจำ) และการจัดสรรทรัพยากรใหม่
หากทั้งสองมีความคล้ายคลึงกันนั่นก็คือผู้ดำเนินการมอบหมายจะทำการทำลายและสร้างสำเนา นักพัฒนาบางคนเคยใช้การมอบหมายงานจริงโดยการทำลายในสถานที่ตามด้วยการสร้างสำเนาตำแหน่ง อย่างไรก็ตามนี่เป็นความคิดที่แย่มาก (จะเกิดอะไรขึ้นถ้านี่คือตัวดำเนินการกำหนดของคลาสพื้นฐานที่เรียกระหว่างการกำหนดคลาสที่ได้รับมา)
สิ่งที่มักถือว่าเป็นสำนวนที่เป็นที่ยอมรับในปัจจุบันใช้swap
ตามที่ Charles แนะนำ:
MyClass& operator=(MyClass other)
{
swap(other);
return *this;
}
สิ่งนี้ใช้การสร้างสำเนา (หมายเหตุที่other
คัดลอก) และการทำลาย (ถูกทำลายเมื่อสิ้นสุดฟังก์ชัน) - และใช้ตามลำดับที่ถูกต้องเช่นกัน: การก่อสร้าง (อาจล้มเหลว) ก่อนการทำลาย (ต้องไม่ล้มเหลว)
มีบางอย่างรบกวนฉันเกี่ยวกับ:
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;
}
ยังคงมีการสร้างวัตถุชั่วคราว แต่การดำเนินการในทันทีต่อไปคือการปลดปล่อยทรัพยากรปัจจุบันทั้งหมดของปลายทางก่อนที่จะย้าย (และเป็นโมฆะเพื่อไม่ให้ทรัพยากรของแหล่งที่มาเป็นอิสระสองเท่า)
แทนที่จะเป็น {สร้างย้ายทำลาย} ฉันเสนอ {สร้างทำลายย้าย} การเคลื่อนไหวซึ่งเป็นการกระทำที่อันตรายที่สุดเป็นการกระทำครั้งสุดท้ายหลังจากที่ทุกอย่างถูกตัดสิน
ใช่ความล้มเหลวในการทำลายล้างเป็นปัญหาในทั้งสองโครงการ ข้อมูลอาจเสียหาย (คัดลอกโดยที่คุณไม่คิดว่าเป็นข้อมูล) หรือสูญหาย (เป็นอิสระเมื่อคุณไม่คิดว่าเป็นข้อมูล) หายดีกว่าเสียหาย ไม่มีข้อมูลใดดีไปกว่าข้อมูลที่ไม่ดี
โอนแทนการแลกเปลี่ยน นั่นเป็นคำแนะนำของฉันอยู่แล้ว
First, reading the word "swap" when my mind is thinking "copy" irritates
-> เป็นนักเขียนห้องสมุดคุณจะรู้ว่าการปฏิบัติทั่วไป (คัดลอกแลกเปลี่ยน +) my mind
และปมคือ ความคิดของคุณซ่อนอยู่หลังอินเทอร์เฟซสาธารณะ นั่นคือสิ่งที่เกี่ยวกับโค้ดที่ใช้ซ้ำได้