ย้ายตัวดำเนินการกำหนดและ "if (this! = & rhs)"


126

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

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

คุณต้องการสิ่งเดียวกันสำหรับตัวดำเนินการมอบหมายการย้ายหรือไม่? เคยมีสถานการณ์ที่this == &rhsจะเป็นจริงหรือไม่?

? Class::operator=(Class&& rhs) {
    ?
}

12
ไม่เกี่ยวข้องกับคำถามที่ถูกถาม & เพียงเพื่อให้ผู้ใช้ใหม่ที่อ่าน Q นี้ลงไทม์ไลน์ (เพราะฉันรู้ว่า Seth รู้เรื่องนี้แล้ว) จะไม่เข้าใจความคิดที่ผิดการคัดลอกและสลับเป็นวิธีที่ถูกต้องในการใช้ตัวดำเนินการคัดลอกการมอบหมาย ไม่จำเป็นต้องตรวจสอบการมอบหมายงานด้วยตนเองและทั้งหมด
Alok Save

5
A a; a = std::move(a);@VaughnCato:
Xeo

11
@VaughnCato การใช้งานstd::moveเป็นเรื่องปกติ จากนั้นคำนึงถึงนามแฝงและเมื่อคุณอยู่ลึกเข้าไปในกลุ่มการโทรและคุณมีการอ้างอิงTหนึ่งรายการและอีกรายการหนึ่งอ้างอิงถึงT... คุณจะตรวจสอบตัวตนที่นี่หรือไม่? คุณต้องการค้นหาการโทรครั้งแรก (หรือการโทร) ที่การบันทึกว่าคุณไม่สามารถส่งผ่านอาร์กิวเมนต์เดียวกันสองครั้งจะพิสูจน์ได้อย่างคงที่ว่าการอ้างอิงทั้งสองนั้นจะไม่ใช้นามแฝงหรือไม่? หรือคุณจะมอบหมายงานด้วยตนเองเพียงแค่ทำงาน?
Luc Danton

2
@LucDanton ฉันต้องการการยืนยันในตัวดำเนินการมอบหมาย หากใช้ std :: move ในลักษณะที่เป็นไปได้ที่จะจบลงด้วยการกำหนดค่า rvalue ด้วยตนเองฉันจะถือว่าเป็นจุดบกพร่องที่ควรได้รับการแก้ไข
Vaughn Cato

4
@VaughnCato สถานที่หนึ่งที่การแลกเปลี่ยนตัวเองเป็นเรื่องปกติอยู่ภายในอย่างใดอย่างหนึ่งstd::sortหรือstd::shuffle- เมื่อใดก็ตามที่คุณสลับองค์ประกอบ th iและjth ของอาร์เรย์โดยไม่ตรวจสอบi != jก่อน ( std::swapดำเนินการในแง่ของการมอบหมายการย้าย)
Quuxplusone

คำตอบ:


143

ว้าวมีอะไรให้ทำความสะอาดมากมายที่นี่ ...

ประการแรก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 มันเป็นหนึ่งในสองสิ่ง:

  1. ชั่วคราว.
  2. วัตถุที่ผู้โทรต้องการให้คุณเชื่อว่าเป็นของชั่วคราว

หากคุณมีการอ้างอิงถึงออบเจ็กต์ที่เป็นวัตถุชั่วคราวตามความหมายคุณจะมีการอ้างอิงเฉพาะสำหรับอ็อบเจ็กต์นั้น ไม่สามารถอ้างอิงได้จากที่อื่นในโปรแกรมทั้งหมดของคุณ อิอิ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สำหรับลูกค้าของคุณเรียกร้องยกเว้นความปลอดภัยที่แข็งแกร่งชี้ให้พวกเขาไปสู่


6
ฉันไม่รู้ว่าควรรู้สึกอย่างไรกับคำตอบนี้ ทำให้ดูเหมือนว่าการใช้คลาสดังกล่าว (ซึ่งจัดการหน่วยความจำอย่างชัดเจน) เป็นเรื่องปกติที่ต้องทำ เป็นความจริงที่ว่าเมื่อคุณเขียนคลาสดังกล่าวคุณต้องระมัดระวังอย่างมากเกี่ยวกับการรับประกันความปลอดภัยยกเว้นและค้นหาจุดที่ดีสำหรับอินเทอร์เฟซที่กระชับ แต่สะดวก แต่คำถามดูเหมือนจะขอคำแนะนำทั่วไป
Luc Danton

ใช่ฉันไม่เคยใช้ copy-and-swap แน่นอนเพราะมันเสียเวลาสำหรับคลาสที่จัดการทรัพยากรและสิ่งต่างๆ (จะไปทำสำเนาข้อมูลทั้งหมดของคุณอีกทำไม) และขอบคุณสิ่งนี้ตอบคำถามของฉัน
Seth Carnegie

5
downvoted สำหรับข้อเสนอแนะว่าการย้ายที่ได้รับมอบหมายจากที่ตัวเองควรจะเคยยืนยันล้มเหลวหรือผลิตเป็น "ไม่ได้ระบุ" ผล การมอบหมายงานจากตัวเองเป็นกรณีที่ง่ายที่สุดในการทำให้ถูกต้อง หากชั้นเรียนของคุณขัดข้องstd::swap(x,x)เหตุใดฉันจึงควรไว้วางใจให้จัดการการดำเนินการที่ซับซ้อนมากขึ้นได้อย่างถูกต้อง
Quuxplusone

1
@Quuxplusone: ฉันเห็นด้วยกับคุณในการยืนยัน - ล้มเหลวดังที่ระบุไว้ในการอัปเดตคำตอบของฉัน เท่าที่std::swap(x,x)ผ่านมามันก็ใช้งานได้แม้ว่าจะx = std::move(x)ให้ผลลัพธ์ที่ไม่ระบุรายละเอียดก็ตาม ลองมัน! คุณไม่จำเป็นต้องเชื่อฉัน
Howard Hinnant

จุดที่ดี @HowardHinnant, swapทำงานตราบเท่าที่x = move(x)ใบxในรัฐย้ายเข้าสามารถใด ๆ และstd::copy/ std::moveอัลกอริทึมถูกกำหนดเพื่อสร้างพฤติกรรมที่ไม่ได้กำหนดไว้ในสำเนาที่ไม่ได้ใช้งานแล้ว (อุ๊ยเด็กอายุ 20 ปีmemmoveได้รับกรณีที่std::moveไม่สำคัญแต่ไม่ถูกต้อง!) ดังนั้นฉันเดาว่าฉันยังไม่ได้คิด "slam dunk" สำหรับการมอบหมายงานด้วยตนเอง แต่เห็นได้ชัดว่าการมอบหมายงานด้วยตนเองเป็นสิ่งที่เกิดขึ้นมากมายในรหัสจริงไม่ว่ามาตรฐานจะอวยพรหรือไม่ก็ตาม
Quuxplusone

11

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

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

โปรดทราบว่าคุณยังคงส่งคืนผ่านการอ้างอิง (ไม่ใช่const) l -value

สำหรับการมอบหมายโดยตรงทั้งสองประเภทมาตรฐานจะไม่ตรวจสอบการมอบหมายงานด้วยตนเอง แต่เพื่อให้แน่ใจว่าการมอบหมายงานด้วยตนเองจะไม่ทำให้เกิดข้อขัดข้องและเบิร์น โดยทั่วไปไม่มีใครทำx = xหรือy = std::move(y)เรียกอย่างชัดเจนแต่การใช้นามแฝงโดยเฉพาะอย่างยิ่งผ่านฟังก์ชั่นต่างๆอาจนำไปสู่a = bหรือc = std::move(d)เป็นการกำหนดตนเอง การตรวจสอบการมอบหมายงานด้วยตนเองอย่างชัดเจนกล่าวคือการthis == &rhsข้ามเนื้อของฟังก์ชันเมื่อเป็นจริงเป็นวิธีหนึ่งในการรับรองความปลอดภัยในการมอบหมายงานด้วยตนเอง แต่เป็นวิธีที่เลวร้ายที่สุดวิธีหนึ่งเนื่องจากเพิ่มประสิทธิภาพกรณีที่หายาก (หวังว่า) ในขณะที่เป็นการป้องกันการเพิ่มประสิทธิภาพสำหรับกรณีทั่วไป (เนื่องจากการแยกสาขาและแคชอาจพลาด)

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

มาดูตัวอย่างโดยดัดแปลงจากผู้ตอบคนอื่น:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

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

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

ลองดูการมอบหมายการย้ายสำหรับประเภทเดียวกันนี้:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

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

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

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

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

ปัญหานี้เป็นสาเหตุของคำแนะนำของกูรูในปัจจุบันที่เป็นที่ถกเถียงกันเกี่ยวกับการกำหนดเป้าหมายตนเองระหว่างการย้ายงาน วิธีการเขียนการมอบหมายการย้ายโดยไม่ให้ทรัพยากรค้างอยู่มีดังนี้:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

แหล่งที่มาจะถูกรีเซ็ตเป็นเงื่อนไขเริ่มต้นในขณะที่ทรัพยากรปลายทางเก่าจะถูกทำลาย ในกรณีการมอบหมายงานด้วยตนเองสิ่งของในปัจจุบันของคุณจะจบลงด้วยการฆ่าตัวตาย วิธีหลักคือล้อมรอบรหัสการดำเนินการด้วยif(this != &other)บล็อกหรือขันสกรูแล้วปล่อยให้ลูกค้ากินassert(this != &other)บรรทัดเริ่มต้น (ถ้าคุณรู้สึกดี)

อีกทางเลือกหนึ่งคือการศึกษาวิธีการกำหนดข้อยกเว้นอย่างยิ่งให้ปลอดภัยโดยไม่ต้องมอบหมายแบบรวมและนำไปใช้กับการย้ายการมอบหมาย:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

เมื่อใดotherและthisมีความแตกต่างotherจะว่างเปล่าโดยการย้ายไปtempและยังคงเป็นเช่นนั้น จากนั้นก็thisจะสูญเสียทรัพยากรเก่าของตนที่จะได้รับในขณะทรัพยากรที่จัดขึ้นครั้งแรกโดยtemp otherจากนั้นทรัพยากรเก่าที่thisถูกฆ่าเมื่อtempไหร่

เมื่อการมอบหมายงานเกิดขึ้นด้วยตนเองการปล่อยotherให้tempว่างเปล่าthisเช่นกัน จากนั้นวัตถุเป้าหมายจะได้รับทรัพยากรคืนเมื่อใดtempและทำการthisสลับ การเสียชีวิตของการtempอ้างว่าเป็นวัตถุว่างเปล่าซึ่งควรจะเป็นสิ่งที่ไม่ควรทำ this/ otherวัตถุช่วยให้ทรัพยากรที่มีอยู่

การมอบหมายการย้ายไม่ควรโยนตราบใดที่มีการย้ายโครงสร้างและการแลกเปลี่ยน ค่าใช้จ่ายในการรักษาความปลอดภัยในระหว่างการมอบหมายงานด้วยตนเองคือคำแนะนำเพิ่มเติมอีกสองสามประการสำหรับประเภทระดับต่ำซึ่งควรจะล้นมือจากการเรียกการจัดสรร


คุณจำเป็นต้องตรวจสอบว่ามีการจัดสรรหน่วยความจำก่อนที่จะเรียกใช้ deleteรหัสบล็อกที่สองหรือไม่?
user3728501

3
ตัวอย่างรหัสที่สองของคุณซึ่งเป็นตัวดำเนินการกำหนดสำเนาโดยไม่มีการตรวจสอบการมอบหมายตนเองผิด std::copyทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดหากช่วงต้นทางและปลายทางทับซ้อนกัน (รวมถึงกรณีที่ตรงกัน) ดู C ++ 14 [alg.copy] / 3
MM

6

ผมอยู่ในค่ายของผู้ที่ต้องการประกอบการปลอดภัยด้วยตนเองได้รับมอบหมาย operator=แต่ไม่ต้องการที่จะเขียนการตรวจสอบด้วยตนเองได้รับมอบหมายในการใช้งานของ และอันที่จริงฉันไม่ต้องการใช้เลยด้วยซ้ำoperator=ฉันต้องการให้พฤติกรรมเริ่มต้นทำงานได้ทันที 'ทันที' สมาชิกพิเศษที่ดีที่สุดคือสมาชิกที่มาฟรี

ดังที่กล่าวไว้ข้อกำหนด MoveAssignable ที่มีอยู่ใน Standard ได้อธิบายไว้ดังต่อไปนี้ (จาก 17.6.3.1 ข้อกำหนดอาร์กิวเมนต์แม่แบบ [utility.arg.requirements], n3290):

Expression Return type Return value Post-condition
t = rv T & tt เทียบเท่ากับค่าของ rv ก่อนการกำหนด

โดยที่ตัวยึดตำแหน่งถูกอธิบายว่า: " t[คือ] ค่าที่ปรับเปลี่ยนได้ของประเภท T;" และ " rvเป็นค่า r ประเภท T;" โปรดทราบว่าข้อกำหนดเหล่านี้เป็นข้อกำหนดที่ระบุไว้ในประเภทที่ใช้เป็นอาร์กิวเมนต์ของเทมเพลตของไลบรารีมาตรฐาน แต่เมื่อมองหาที่อื่นใน Standard ฉันสังเกตเห็นว่าข้อกำหนดในการย้ายทุกข้อจะคล้ายกับข้อนี้

ซึ่งหมายความว่าa = std::move(a)จะต้อง 'ปลอดภัย' หากสิ่งที่คุณต้องการคือการทดสอบตัวตน (เช่นthis != &other) ให้ทำไม่เช่นนั้นคุณจะไม่สามารถใส่วัตถุของคุณลงไปได้std::vector! (ยกเว้นกรณีที่คุณไม่ได้ใช้บรรดาสมาชิก / การดำเนินงานที่จะต้อง MoveAssignable. แต่ไม่เป็นไรนั้น) ขอให้สังเกตว่ามีตัวอย่างก่อนหน้านี้a = std::move(a)แล้วthis == &otherจะแน่นอนถือ


คุณอธิบายได้ไหมว่าการa = std::move(a)ไม่ทำงานจะทำให้ชั้นเรียนไม่ทำงานได้std::vectorอย่างไร ตัวอย่าง?
Paul

@ PaulJ.Lucas Calling std::vector<T>::eraseไม่ได้รับอนุญาตเว้นแต่Tจะ MoveAssignable (เนื่องจากนอกเหนือจาก IIRC ข้อกำหนดของ MoveAssignable บางอย่างได้รับการผ่อนปรนให้กับ MoveInsertable แทนใน C ++ 14)
Luc Danton

ตกลงมันTต้องเป็น MoveAssignable แต่ทำไมต้องerase()ขึ้นอยู่กับการย้ายองค์ประกอบไปเอง ?
Paul

@ PaulJ.Lucas ไม่มีคำตอบที่น่าพอใจสำหรับคำถามนั้น ทุกอย่างเดือดจน 'อย่าทำลายสัญญา'
Luc Danton

2

เนื่องจากoperator=ฟังก์ชันปัจจุบันของคุณถูกเขียนขึ้นเนื่องจากคุณได้สร้างอาร์กิวเมนต์อ้างอิง rvalue constจึงไม่มีทางที่คุณจะ "ขโมย" พอยน์เตอร์และเปลี่ยนค่าของการอ้างอิงค่า rvalue ที่เข้ามาได้ ... คุณก็ไม่สามารถเปลี่ยนแปลงได้ สามารถอ่านได้จากมันเท่านั้น ฉันจะเห็นปัญหาก็ต่อเมื่อคุณเริ่มเรียกdeleteใช้พอยน์เตอร์ ฯลฯ ในthisวัตถุของคุณเช่นเดียวกับที่คุณทำในoperator=วิธีอ้างอิง lvaue ปกติแต่ประเภทของการเอาชนะจุดของ rvalue-version ... กล่าวคือมันจะ ดูเหมือนจะซ้ำซ้อนในการใช้เวอร์ชัน rvalue เพื่อทำการดำเนินการเดียวกันโดยปกติแล้วจะเหลือวิธีการconst-lvalueoperator=

ตอนนี้ถ้าคุณกำหนดให้คุณoperator=ใช้การconstอ้างอิงที่ไม่ใช่rvalue วิธีเดียวที่ฉันจะเห็นว่าจำเป็นต้องมีการตรวจสอบคือถ้าคุณส่งthisวัตถุไปยังฟังก์ชันที่ส่งคืนการอ้างอิงค่า rvalue โดยเจตนาแทนที่จะเป็นการชั่วคราว

ตัวอย่างเช่นสมมติว่ามีคนพยายามเขียนoperator+ฟังก์ชันและใช้การอ้างอิง rvalue และการอ้างอิง lvalue ผสมกันเพื่อ "ป้องกัน" ไม่ให้มีการสร้าง temporaries เพิ่มเติมในระหว่างการดำเนินการเพิ่มเติมแบบเรียงซ้อนบน object-type:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

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


โปรดทราบว่า "a = std :: move (a)" เป็นวิธีที่ไม่สำคัญในการทำให้เกิดสถานการณ์เช่นนี้ คำตอบของคุณถูกต้องแม้ว่า
Vaughn Cato

1
เห็นด้วยอย่างยิ่งว่าเป็นวิธีที่ง่ายที่สุดแม้ว่าฉันจะคิดว่าคนส่วนใหญ่จะไม่ทำอย่างนั้นโดยเจตนา :-) ... โปรดทราบว่าหาก rvalue-reference เป็นconstคุณสามารถอ่านได้จากมันเท่านั้นดังนั้นจึงจำเป็นต้อง ตรวจสอบจะเป็นถ้าคุณตัดสินใจที่operator=(const T&&)จะดำเนินการเริ่มต้นใหม่แบบเดียวกับthisที่คุณจะทำในoperator=(const T&)วิธีการทั่วไปแทนที่จะเป็นรูปแบบการแลกเปลี่ยน (เช่นการขโมยพอยน์เตอร์ ฯลฯ แทนที่จะทำสำเนาลึก)
Jason

1

คำตอบของฉันยังคงอยู่ที่การมอบหมายการย้ายไม่จำเป็นต้องประหยัดจากการมอบหมายงานด้วยตนเอง แต่มีคำอธิบายที่แตกต่างออกไป พิจารณา std :: unique_ptr ถ้าฉันจะใช้มันฉันจะทำสิ่งนี้:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

หากคุณดูที่Scott Meyers อธิบายสิ่งนี้เขาก็ทำสิ่งที่คล้ายกัน (ถ้าคุณหลงทำไมไม่ทำ swap - มันมีการเขียนพิเศษ) และไม่ปลอดภัยสำหรับการมอบหมายงานด้วยตนเอง

บางครั้งนี่เป็นเรื่องโชคร้าย พิจารณาย้ายออกจากเวกเตอร์เลขคู่ทั้งหมด:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

นี่ใช้ได้สำหรับจำนวนเต็ม แต่ฉันไม่เชื่อว่าคุณจะทำอะไรแบบนี้ได้ด้วยความหมายการเคลื่อนที่

สรุป: การย้ายการมอบหมายงานไปยังวัตถุนั้นไม่โอเคและคุณต้องระวังมัน

อัปเดตขนาดเล็ก

  1. ฉันไม่เห็นด้วยกับ Howard ซึ่งเป็นความคิดที่ไม่ดี แต่ก็ยัง - ฉันคิดว่าการมอบหมายวัตถุที่ "ย้ายออก" ด้วยตนเองควรได้ผลเพราะswap(x, x)ควรจะได้ผล อัลกอริทึมรักสิ่งเหล่านี้! มันดีเสมอเมื่อเคสเข้ามุมใช้งานได้จริง (และฉันยังไม่เห็นกรณีที่มันไม่ฟรีไม่ได้หมายความว่ามันไม่มีอยู่จริง)
  2. นี่คือวิธีการใช้งานการกำหนด unique_ptrs ใน libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} ปลอดภัยสำหรับการมอบหมายการย้ายตัวเอง
  3. แนวทางหลักคิดว่าควรจะมอบหมายให้ย้ายด้วยตนเอง

0

มีสถานการณ์ที่ (สิ่งนี้ == rhs) ฉันคิดได้ สำหรับคำสั่งนี้: Myclass obj; std :: move (obj) = std :: move (obj)


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