ว้าวมีอะไรให้ทำความสะอาดมากมายที่นี่ ...
ประการแรกCopy and Swapไม่ใช่วิธีที่ถูกต้องในการใช้งาน Copy Assignment เกือบจะแน่นอนในกรณีdumb_array
นี้เป็นวิธีแก้ปัญหาที่ไม่เหมาะสม
การใช้Copy and Swapเป็นdumb_array
ตัวอย่างคลาสสิกของการใช้งานที่แพงที่สุดพร้อมคุณสมบัติที่สมบูรณ์ที่สุดที่เลเยอร์ด้านล่าง เหมาะสำหรับลูกค้าที่ต้องการคุณสมบัติที่ครบถ้วนที่สุดและยินดีจ่ายค่าปรับตามประสิทธิภาพ พวกเขาได้สิ่งที่ต้องการ
แต่มันเป็นหายนะสำหรับลูกค้าที่ไม่ต้องการคุณสมบัติที่สมบูรณ์ที่สุดและมองหาประสิทธิภาพสูงสุดแทน สำหรับพวกเขาdumb_array
เป็นเพียงส่วนหนึ่งของซอฟต์แวร์ที่พวกเขาต้องเขียนใหม่เพราะมันช้าเกินไป ได้dumb_array
รับการออกแบบที่แตกต่างกันจึงสามารถสร้างความพึงพอใจให้กับลูกค้าทั้งสองฝ่ายโดยไม่มีการประนีประนอมกับลูกค้าทั้งสองราย
กุญแจสำคัญในการสร้างความพึงพอใจให้กับลูกค้าทั้งสองคือการสร้างการดำเนินงานที่เร็วที่สุดในระดับต่ำสุดจากนั้นเพิ่ม API เพื่อให้ได้คุณสมบัติที่สมบูรณ์ยิ่งขึ้นโดยมีค่าใช้จ่ายมากขึ้น กล่าวคือคุณต้องการการรับประกันข้อยกเว้นที่เข้มงวดดีคุณจ่ายสำหรับมัน คุณไม่ต้องการมันเหรอ? นี่เป็นวิธีแก้ปัญหาที่เร็วกว่า
ให้เป็นรูปธรรม: นี่คือตัวดำเนินการกำหนดข้อยกเว้นพื้นฐานที่รวดเร็วและรับประกันสำหรับdumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
คำอธิบาย:
สิ่งหนึ่งที่มีราคาแพงกว่าที่คุณสามารถทำได้กับฮาร์ดแวร์สมัยใหม่คือการเดินทางไปยังฮีป สิ่งที่คุณทำได้เพื่อหลีกเลี่ยงการเดินทางไปยังฮีปคือการใช้เวลาและความพยายามอย่างดี ลูกค้าของdumb_array
อาจต้องการกำหนดอาร์เรย์ที่มีขนาดเท่ากันบ่อยๆ และเมื่อพวกเขาทำสิ่งที่คุณต้องทำก็คือmemcpy
(ซ่อนอยู่ข้างใต้std::copy
) คุณไม่ต้องการจัดสรรอาร์เรย์ใหม่ที่มีขนาดเท่ากันจากนั้นจึงจัดสรรอาร์เรย์เก่าที่มีขนาดเท่ากัน!
ตอนนี้สำหรับลูกค้าของคุณที่ต้องการความปลอดภัยจากข้อยกเว้นที่แข็งแกร่ง:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
หรือบางทีถ้าคุณต้องการใช้ประโยชน์จากการมอบหมายการย้ายใน C ++ 11 ที่ควรจะเป็น:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
หากdumb_array
ลูกค้าให้ความสำคัญกับความเร็วพวกเขาควรเรียกใช้ไฟล์operator=
. หากพวกเขาต้องการความปลอดภัยจากข้อยกเว้นที่แข็งแกร่งมีอัลกอริทึมทั่วไปที่สามารถเรียกใช้ซึ่งจะทำงานกับวัตถุหลากหลายประเภทและจำเป็นต้องใช้เพียงครั้งเดียว
ตอนนี้กลับไปที่คำถามเดิม (ซึ่งมี type-o ณ เวลานี้):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
นี่เป็นคำถามที่ถกเถียงกันอยู่ บางคนจะตอบว่าใช่บางคนก็ตอบว่าไม่
ความคิดเห็นส่วนตัวของฉันคือไม่คุณไม่จำเป็นต้องตรวจสอบนี้
เหตุผล:
เมื่อวัตถุเชื่อมโยงกับการอ้างอิง rvalue มันเป็นหนึ่งในสองสิ่ง:
- ชั่วคราว.
- วัตถุที่ผู้โทรต้องการให้คุณเชื่อว่าเป็นของชั่วคราว
หากคุณมีการอ้างอิงถึงออบเจ็กต์ที่เป็นวัตถุชั่วคราวตามความหมายคุณจะมีการอ้างอิงเฉพาะสำหรับอ็อบเจ็กต์นั้น ไม่สามารถอ้างอิงได้จากที่อื่นในโปรแกรมทั้งหมดของคุณ อิอิthis == &temporary
ไม่ได้ .
ตอนนี้ถ้าลูกค้าของคุณโกหกคุณและสัญญากับคุณว่าคุณจะได้รับชั่วคราวเมื่อคุณไม่อยู่ก็เป็นความรับผิดชอบของลูกค้าที่จะต้องแน่ใจว่าคุณไม่ต้องดูแล หากคุณต้องการระมัดระวังจริงๆฉันเชื่อว่านี่จะเป็นการใช้งานที่ดีกว่า:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
เช่นถ้าคุณจะผ่านการอ้างอิงตัวเองนี้เป็นข้อผิดพลาดในส่วนของลูกค้าที่ควรได้รับการแก้ไข
เพื่อความสมบูรณ์นี่คือตัวดำเนินการกำหนดการย้ายสำหรับdumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
ในกรณีการใช้งานทั่วไปของการมอบหมายการย้าย*this
จะเป็นวัตถุที่ย้ายจากและdelete [] mArray;
ควรเป็นแบบไม่ใช้งาน เป็นสิ่งสำคัญอย่างยิ่งที่การใช้งานจะทำการลบบน nullptr ให้เร็วที่สุด
ข้อแม้:
บางคนอาจโต้แย้งว่านั่นswap(x, x)
เป็นความคิดที่ดีหรือเป็นเพียงความชั่วที่จำเป็น และสิ่งนี้หากการแลกเปลี่ยนไปที่การแลกเปลี่ยนเริ่มต้นอาจทำให้เกิดการมอบหมายการย้ายตัวเอง
ผมไม่เห็นด้วยที่swap(x, x)
เป็นเคยเป็นความคิดที่ดี หากพบในโค้ดของฉันฉันจะพิจารณาว่าเป็นจุดบกพร่องด้านประสิทธิภาพและแก้ไข แต่ในกรณีที่คุณต้องการอนุญาตโปรดทราบว่าswap(x, x)
มีเพียง self-move-assignemnet กับค่าที่ย้ายจากเท่านั้น และในdumb_array
ตัวอย่างของเราสิ่งนี้จะไม่เป็นอันตรายอย่างสมบูรณ์หากเราเพียงแค่ละเว้นการยืนยันหรือ จำกัด ให้เป็นกรณีที่ย้ายจาก:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
หากคุณกำหนดค่าการย้ายจาก (ว่าง) สองรายการด้วยตัวเองdumb_array
คุณจะไม่ทำอะไรผิดพลาดนอกเหนือจากการใส่คำสั่งที่ไร้ประโยชน์ลงในโปรแกรมของคุณ การสังเกตเดียวกันนี้สามารถทำได้กับวัตถุส่วนใหญ่
<
ปรับปรุง>
ฉันได้ให้ความคิดกับปัญหานี้มากขึ้นและเปลี่ยนตำแหน่งของฉันไปบ้าง ตอนนี้ฉันเชื่อว่าการมอบหมายงานควรอดทนต่อการมอบหมายงานด้วยตนเอง แต่เงื่อนไขการโพสต์ในการมอบหมายสำเนาและการย้ายงานนั้นแตกต่างกัน:
สำหรับการกำหนดสำเนา:
x = y;
ควรมีเงื่อนไขการโพสต์ที่y
ไม่ควรเปลี่ยนแปลงค่า เมื่อ&x == &y
แล้ว postcondition นี้แปลเป็น: x
ตนเองได้รับมอบหมายสำเนาควรจะมีผลกระทบต่อมูลค่าของไม่มี
สำหรับการมอบหมายการย้าย:
x = std::move(y);
ควรมี post-condition ที่y
มีสถานะที่ถูกต้อง แต่ไม่ได้ระบุ เมื่อ&x == &y
นั้นเงื่อนไขภายหลังนี้จะแปลเป็น: x
มีสถานะที่ถูกต้อง แต่ไม่ได้ระบุ กล่าวคือการมอบหมายการย้ายตนเองไม่จำเป็นต้องเป็นแบบไม่ต้องดำเนินการ แต่ก็ไม่ควรผิดพลาด เงื่อนไขหลังนี้สอดคล้องกับการอนุญาตให้swap(x, x)
ใช้งานได้:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
ข้างต้นใช้งานได้ตราบเท่าที่x = std::move(x)
ไม่ผิดพลาด สามารถปล่อยให้x
อยู่ในสถานะที่ถูกต้อง แต่ไม่ระบุ
ฉันเห็นสามวิธีในการตั้งโปรแกรมตัวดำเนินการกำหนดย้ายdumb_array
เพื่อให้บรรลุสิ่งนี้:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
การดำเนินการดังกล่าวข้างต้นทนการกำหนดตัวเอง แต่*this
และother
จบลงด้วยการอาร์เรย์เป็นศูนย์กลางหลังจากที่ได้รับมอบหมายด้วยตนเองย้ายไม่ว่าสิ่งที่ค่าเดิมของ*this
มี แค่นี้ก็เรียบร้อย
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
การใช้งานข้างต้นทนต่อการมอบหมายงานด้วยตนเองในลักษณะเดียวกับที่ตัวดำเนินการกำหนดสำเนาทำโดยกำหนดให้เป็นแบบไม่ใช้ นี่ก็ดีเหมือนกัน
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
ข้างต้นใช้ได้เฉพาะในกรณีที่dumb_array
ไม่ได้ถือทรัพยากรที่ควรถูกทำลาย "ทันที" ตัวอย่างเช่นถ้าทรัพยากรเดียวคือหน่วยความจำข้างต้นก็ใช้ได้ หากdumb_array
สามารถล็อค mutex หรือสถานะเปิดของไฟล์ไคลเอนต์สามารถคาดหวังได้อย่างสมเหตุสมผลว่าทรัพยากรเหล่านั้นใน lhs ของการมอบหมายการย้ายจะถูกปล่อยทันทีดังนั้นการใช้งานนี้อาจเป็นปัญหาได้
ต้นทุนของร้านแรกคือร้านค้าพิเศษสองแห่ง ค่าใช้จ่ายที่สองคือการทดสอบและสาขา ทำงานทั้งคู่. ทั้งสองเป็นไปตามข้อกำหนดทั้งหมดของตารางที่ 22 ข้อกำหนด MoveAssignable ในมาตรฐาน C ++ 11 อย่างที่สามยังทำงานแบบโมดูโลสำหรับปัญหาที่ไม่ใช่หน่วยความจำทรัพยากร
การใช้งานทั้งสามอย่างอาจมีต้นทุนที่แตกต่างกันขึ้นอยู่กับฮาร์ดแวร์: สาขามีราคาแพงแค่ไหน? มีการลงทะเบียนจำนวนมากหรือน้อยมาก?
สิ่งที่ต้องซื้อกลับไปคือการมอบหมายงานด้วยตนเองซึ่งแตกต่างจากการกำหนดสำเนาด้วยตนเองไม่จำเป็นต้องรักษาค่าปัจจุบันไว้
<
/ อัปเดต>
การแก้ไขขั้นสุดท้าย (หวังว่า) จะได้รับแรงบันดาลใจจากความคิดเห็นของ Luc Danton:
หากคุณกำลังเขียนคลาสระดับสูงที่ไม่ได้จัดการหน่วยความจำโดยตรง (แต่อาจมีฐานหรือสมาชิกที่ทำได้) การใช้งานการมอบหมายการย้ายที่ดีที่สุดมักจะ:
Class& operator=(Class&&) = default;
สิ่งนี้จะย้ายกำหนดแต่ละฐานและสมาชิกแต่ละคนในทางกลับกันและจะไม่รวมthis != &other
เช็ค สิ่งนี้จะทำให้คุณได้รับประสิทธิภาพสูงสุดและความปลอดภัยในการยกเว้นขั้นพื้นฐานโดยสมมติว่าไม่จำเป็นต้องรักษาค่าคงที่ระหว่างฐานและสมาชิกของคุณ strong_assign
สำหรับลูกค้าของคุณเรียกร้องยกเว้นความปลอดภัยที่แข็งแกร่งชี้ให้พวกเขาไปสู่