สำนวนคัดลอกและแลกเปลี่ยนคืออะไร?


2001

สำนวนนี้คืออะไรและควรใช้เมื่อใด ปัญหาอะไรที่แก้ได้? สำนวนเปลี่ยนไปเมื่อใช้ C ++ 11 หรือไม่?

แม้ว่ามันจะถูกกล่าวถึงในหลาย ๆ ที่ แต่เราก็ไม่มีคำถามและคำตอบว่า "มันคืออะไร" มันจึงเป็นเช่นนี้ นี่คือรายการบางส่วนของสถานที่ที่เคยกล่าวถึง:


7
gotw.ca/gotw/059.htmจาก Herb Sutter
DumbCoder

2
น่ากลัวฉันเชื่อมโยงคำถามนี้จากฉันคำตอบให้กับความหมายย้าย
fredoverflow

4
ความคิดที่ดีที่จะมีคำอธิบายที่สมบูรณ์สำหรับสำนวนนี้มันเป็นเรื่องธรรมดาที่ทุกคนควรรู้
Matthieu M.

16
คำเตือน: สำนวนคัดลอก / swap จะใช้บ่อยกว่าที่เป็นประโยชน์ บ่อยครั้งเป็นอันตรายต่อประสิทธิภาพเมื่อไม่จำเป็นต้องมีการรับประกันความปลอดภัยยกเว้นข้อยกเว้นจากการมอบหมายให้ทำสำเนา และเมื่อต้องการความปลอดภัยของข้อยกเว้นที่คาดเดายากสำหรับการมอบหมายให้ทำสำเนามันมีฟังก์ชั่นทั่วไปสั้น ๆ ให้ได้อย่างง่ายดายนอกเหนือจากผู้ปฏิบัติงานมอบหมายการคัดลอกที่เร็วกว่ามาก ดูslideshare.net/ripplelabs/howard-hinnant-accu2014สไลด์ 43 - 53 บทสรุป: copy / swap เป็นเครื่องมือที่มีประโยชน์ในกล่องเครื่องมือ แต่มันมีการทำตลาดมากเกินไปและต่อมามักถูกทำผิดกฎเกี่ยว
Howard Hinnant

2
@HowardHinnant: ใช่ +1 ถึงสิ่งนั้น ฉันเขียนสิ่งนี้ในเวลาที่เกือบทุกคำถาม C ++ คือ "ช่วยชั้นเรียนของฉันล่มเมื่อคัดลอก" และนี่คือคำตอบของฉัน มันเหมาะสมเมื่อคุณแค่ต้องการ copy- / move-semantics หรืออะไรก็ตามเพื่อให้คุณสามารถไปยังสิ่งอื่น ๆ ได้ แต่มันก็ไม่ได้ดีที่สุด รู้สึกอิสระที่จะปฏิเสธความรับผิดชอบที่ด้านบนของคำตอบของฉันถ้าคุณคิดว่าจะช่วย
GManNickG

คำตอบ:


2184

ภาพรวม

ทำไมเราต้องใช้สำนวนคัดลอกและแลกเปลี่ยน?

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

สำนวนการคัดลอกและการแลกเปลี่ยนคือการแก้ปัญหาและสวยงามช่วยให้ผู้ประกอบการที่ได้รับมอบหมายในการบรรลุสิ่งที่สอง: การหลีกเลี่ยงความซ้ำซ้อนรหัสและการให้การรับประกันข้อยกเว้นที่แข็งแกร่ง

มันทำงานยังไง?

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

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

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

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


คำอธิบายในเชิงลึก

เป้าหมาย

ลองพิจารณากรณีที่เป็นรูปธรรม เราต้องการจัดการในอาเรย์แบบไดนามิกที่ไร้ประโยชน์ เราเริ่มต้นด้วย constructor ที่ใช้งานได้ copy-constructor และ destructor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

คลาสนี้จัดการอาร์เรย์ได้สำเร็จ แต่ต้องoperator=ทำงานอย่างถูกต้อง

ทางออกที่ล้มเหลว

นี่คือวิธีการใช้งานที่ไร้เดียงสาอาจมีลักษณะ:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

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

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

  2. ประการที่สองคือให้การรับประกันข้อยกเว้นพื้นฐานเท่านั้น หากnew int[mSize]ล้มเหลว*thisจะได้รับการแก้ไข (กล่าวคือขนาดไม่ถูกต้องและข้อมูลหายไป!) สำหรับการรับประกันข้อยกเว้นที่แข็งแกร่งจะต้องเป็นสิ่งที่คล้ายกับ:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
  3. รหัสได้ขยาย! ซึ่งนำเราไปสู่ปัญหาที่สาม: การทำสำเนารหัส ผู้ดำเนินการที่ได้รับมอบหมายของเราทำซ้ำรหัสทั้งหมดที่เราเคยเขียนไว้ที่อื่นอย่างมีประสิทธิภาพและนั่นเป็นสิ่งที่แย่มาก

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

(หนึ่งอาจสงสัย: ถ้ารหัสนี้มากเป็นสิ่งจำเป็นในการจัดการทรัพยากรอย่างถูกต้องสิ่งที่ถ้าชั้นเรียนของฉันจัดการมากกว่าหนึ่งขณะนี้อาจดูเหมือนจะเป็นกังวลที่ถูกต้องและแน่นอนมันต้องไม่น่ารำคาญtry/ catchข้อนี้เป็นที่ไม่ใช่ - ใหม่นั่นเป็นเพราะคลาสควรจัดการหนึ่งทรัพยากรเท่านั้น !)

ทางออกที่ประสบความสำเร็จ

ดังที่กล่าวไว้สำนวนการคัดลอกและสลับจะแก้ไขปัญหาเหล่านี้ทั้งหมด แต่ตอนนี้เรามีข้อกำหนดทั้งหมดยกเว้นหนึ่งswapฟังก์ชัน: a ในขณะที่กฎของสามประสบความสำเร็จสร้างความดำรงอยู่ของเราคัดลอกตัวสร้างผู้ประกอบการที่ได้รับมอบหมายและ destructor ก็ควรจริงๆจะเรียกว่า "บิ๊กสามและครึ่ง" เวลาใด class ของคุณจัดการทรัพยากรก็ยังทำให้ความรู้สึกที่จะให้เป็นswapฟังก์ชั่น .

เราจำเป็นต้องเพิ่มฟังก์ชั่นการแลกเปลี่ยนในชั้นเรียนของเราและเราทำสิ่งดังต่อไปนี้†:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( นี่คือคำอธิบายว่าทำไมpublic friend swap) ตอนนี้ไม่เพียง แต่เราสามารถแลกเปลี่ยนของเราdumb_arrayได้ แต่การแลกเปลี่ยนโดยทั่วไปจะมีประสิทธิภาพมากกว่า มันแค่แลกเปลี่ยนพอยน์เตอร์และขนาดแทนที่จะจัดสรรและคัดลอกอาร์เรย์ทั้งหมด นอกเหนือจากโบนัสนี้ในการทำงานและประสิทธิภาพตอนนี้เราพร้อมที่จะใช้สำนวนการคัดลอกและสลับ

หากไม่มีความกังวลใจเพิ่มเติมผู้ดำเนินการที่ได้รับมอบหมายของเราคือ:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

และนั่นมัน! ด้วยการล้มลงหนึ่งปัญหาทั้งสามได้รับการจัดการอย่างสง่างามในครั้งเดียว

ทำไมมันทำงาน

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

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

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

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

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

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

เนื่องจากสำนวนนั้นไม่มีรหัสซ้ำเราจึงไม่สามารถแนะนำบั๊กภายในโอเปอเรเตอร์ได้ โปรดทราบว่านี่หมายความว่าเราไม่จำเป็นต้องมีการตรวจสอบการมอบหมายด้วยตนเองoperator=อีกต่อไป (นอกจากนี้เราจะไม่ได้รับโทษปรับประสิทธิภาพจากการไม่ได้มอบหมายตนเอง)

และนั่นคือสำนวนคัดลอกและสลับ

แล้ว C ++ 11 ล่ะ

รุ่นถัดไปของ C ++, C ++ 11 ทำให้การเปลี่ยนแปลงสำคัญอย่างหนึ่งสำหรับวิธีการที่เราจัดการทรัพยากร: กฎของสามคือตอนนี้กฎของสี่ (และครึ่ง) ทำไม? เพราะไม่เพียง แต่เราจะต้องมีความสามารถในการคัดลอกสร้างทรัพยากรของเราที่เราจำเป็นต้องย้ายที่สร้างมันให้ดีที่สุด

โชคดีสำหรับเรานี่เป็นเรื่องง่าย:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

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

ดังนั้นสิ่งที่เราทำนั้นง่ายมาก: เริ่มต้นผ่านตัวสร้างปริยาย (คุณสมบัติ C ++ 11) จากนั้นสลับกับother; เรารู้ว่าอินสแตนซ์ที่สร้างขึ้นเป็นค่าเริ่มต้นของคลาสของเราสามารถกำหนดและทำลายได้อย่างปลอดภัยดังนั้นเราจึงรู้ว่าotherจะสามารถทำสิ่งเดียวกันได้หลังจากเปลี่ยน

(โปรดทราบว่าคอมไพเลอร์บางตัวไม่สนับสนุนตัวสร้างคอนสตรัคเตอร์ในกรณีนี้เราต้องเริ่มต้นสร้างคลาสด้วยตนเองนี่เป็นงานที่โชคร้าย แต่โชคดีมาก)

ทำไมจึงใช้งานได้

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

dumb_array& operator=(dumb_array other); // (1)

ตอนนี้ถ้าotherจะถูกเริ่มต้นด้วยการ rvalue, มันจะย้ายที่สร้าง สมบูรณ์ ในทำนองเดียวกัน C ++ 03 ให้เราใช้ฟังก์ชั่นตัวสร้างสำเนาของเราอีกครั้งโดยรับอาร์กิวเมนต์โดยค่า C ++ 11 จะเลือกตัวสร้างการย้ายโดยอัตโนมัติเมื่อเหมาะสมเช่นกัน (และแน่นอนตามที่กล่าวถึงในบทความที่เชื่อมโยงก่อนหน้านี้การคัดลอก / การเคลื่อนย้ายของค่าอาจรวมเข้าด้วยกัน)

ดังนั้นสรุปสำนวนคัดลอกและแลกเปลี่ยน


เชิงอรรถ

* ทำไมเราmArrayถึงตั้งค่าเป็นโมฆะ? เพราะถ้ามีรหัสเพิ่มเติมใด ๆ ในโอเปอเรเตอร์ที่ส่งไปdumb_arrayจะมีการเรียกdestructor ของ และหากสิ่งนั้นเกิดขึ้นโดยไม่ตั้งค่าเป็นโมฆะเราพยายามลบหน่วยความจำที่ถูกลบไปแล้ว! เราหลีกเลี่ยงสิ่งนี้ด้วยการตั้งค่าเป็นโมฆะเนื่องจากการลบโมฆะเป็นการไม่ดำเนินการ

claims มีข้อเรียกร้องอื่น ๆ ที่เราควรมีความเชี่ยวชาญstd::swapสำหรับประเภทของเราจัดให้มีswapฟังก์ชั่นฟรีในชั้นเรียนswapและอื่น ๆ แต่สิ่งนี้ไม่จำเป็นทั้งหมด: การใช้ที่เหมาะสมใด ๆswapจะเป็นการโทรที่ไม่มีเงื่อนไขและฟังก์ชั่นของเราจะ พบได้ผ่านADL ฟังก์ชั่นหนึ่งจะทำ

‡เหตุผลง่าย ๆ : เมื่อคุณมีทรัพยากรให้กับตัวเองแล้วคุณสามารถสลับและ / หรือย้าย (C ++ 11) ได้ทุกที่ที่ต้องการ และโดยการทำสำเนาในรายการพารามิเตอร์คุณเพิ่มประสิทธิภาพสูงสุด

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


17
@GMan: ฉันจะยืนยันว่าคลาสที่จัดการทรัพยากรหลายอย่างพร้อมกันนั้นล้มเหลว (ยกเว้นความปลอดภัยกลายเป็นฝันร้าย) และฉันขอแนะนำอย่างยิ่งว่าคลาสจะจัดการทรัพยากรหนึ่งอย่างหรือมีฟังก์ชั่นทางธุรกิจและใช้ผู้จัดการ
Matthieu M.

22
ฉันไม่เข้าใจว่าทำไมการแลกเปลี่ยนประกาศเป็นเพื่อนที่นี่
szx

9
@asd: เพื่ออนุญาตให้พบผ่าน ADL
GManNickG

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

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

274

การกำหนดที่เป็นหัวใจของมันคือสองขั้นตอน: การฉีกสถานะเก่าของวัตถุและสร้างสถานะใหม่เป็นสำเนาของสถานะของวัตถุอื่น ๆ

โดยพื้นฐานนั่นคือสิ่งที่ผู้ทำลายและผู้สร้างสำเนาทำดังนั้นความคิดแรกคือการมอบหมายงานให้พวกเขา อย่างไรก็ตามเนื่องจากการทำลายจะต้องไม่ล้มเหลวขณะที่การก่อสร้างอาจ, เราจริงต้องการที่จะทำวิธีอื่น ๆ : ครั้งแรกที่ดำเนินการในส่วนที่สร้างสรรค์และหากที่ประสบความสำเร็จแล้วทำส่วนการทำลายล้าง copy-and-swap idiom เป็นวิธีที่จะทำเช่นนั้น: มันเรียกตัวสร้างสำเนาของคลาสเพื่อสร้างวัตถุชั่วคราวจากนั้นทำการแลกเปลี่ยนข้อมูลกับวัตถุชั่วคราวแล้วปล่อยให้ผู้ทำลายชั่วคราวทำลายสถานะเก่า
ตั้งแต่swap()ควรจะไม่ล้มเหลวส่วนเดียวที่อาจล้มเหลวคือการสร้างสำเนา สิ่งนี้ถูกดำเนินการก่อนและหากล้มเหลวจะไม่มีการเปลี่ยนแปลงอะไรในวัตถุเป้าหมาย

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

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}

1
ฉันคิดว่าการกล่าวถึง pimpl นั้นสำคัญเท่ากับการพูดถึงการคัดลอกการแลกเปลี่ยนและการทำลายล้าง การแลกเปลี่ยนไม่ปลอดภัยอย่างน่าอัศจรรย์ มันเป็นข้อยกเว้นที่ปลอดภัยเพราะการสลับพอยน์เตอร์นั้นปลอดภัยยกเว้น คุณไม่จำเป็นต้องใช้ pimpl แต่ถ้าคุณไม่มีก็ต้องทำให้แน่ใจว่าการแลกเปลี่ยนสมาชิกแต่ละครั้งนั้นมีข้อยกเว้นที่ปลอดภัย นั่นอาจเป็นฝันร้ายเมื่อสมาชิกเหล่านี้สามารถเปลี่ยนแปลงได้และมันก็เล็กน้อยเมื่อพวกเขาถูกซ่อนอยู่หลัง pimpl และจากนั้นก็มาถึงต้นทุนของ pimpl ซึ่งนำเราไปสู่ข้อสรุปที่บ่อยครั้งที่ข้อยกเว้นด้านความปลอดภัยมีค่าใช้จ่ายในการปฏิบัติงาน
wilhelmtell

7
std::swap(this_string, that)ไม่รับประกันไม่มีการโยน มันให้ความปลอดภัยข้อยกเว้นที่แข็งแกร่ง แต่ไม่รับประกันไม่มีโยน
wilhelmtell

11
@wilhelmtell: ใน C ++ 03 ไม่มีการกล่าวถึงข้อยกเว้นที่อาจเกิดขึ้นจากstd::string::swap(ซึ่งถูกเรียกโดยstd::swap) ใน C ++ 0x std::string::swapคือnoexceptและต้องไม่ส่งข้อยกเว้น
James McNellis

2
@sbi @JamesMcNellis ตกลง แต่ประเด็นยังคงมีอยู่: หากคุณมีสมาชิกประเภทคลาสคุณต้องทำให้แน่ใจว่าการแลกเปลี่ยนนั้นเป็นแบบไม่ต้องโยน หากคุณมีสมาชิกรายเดียวที่เป็นตัวชี้แสดงว่าเป็นเรื่องเล็กน้อย ไม่เช่นนั้น
wilhelmtell

2
@wilhelmtell: ผมคิดว่าเป็นจุดแลกเปลี่ยน: มันไม่เคยพ่นและมันอยู่เสมอ O (1) (ใช่ฉันรู้std::array... )
เอสบีไอ

44

มีคำตอบที่ดีอยู่แล้ว ผมจะเน้นส่วนใหญ่เกี่ยวกับสิ่งที่ผมคิดว่าพวกเขาขาด - คำอธิบายของ "ข้อเสีย" กับสำนวนการคัดลอกและการแลกเปลี่ยนที่ ....

สำนวนคัดลอกและแลกเปลี่ยนคืออะไร?

วิธีใช้งานโอเปอเรเตอร์การมอบหมายในแง่ของฟังก์ชั่นสลับ

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

แนวคิดพื้นฐานคือ:

  • ส่วนที่เกิดข้อผิดพลาดได้ง่ายที่สุดในการกำหนดให้กับวัตถุคือทำให้แน่ใจว่าทรัพยากรใด ๆ ที่ได้รับความต้องการของรัฐใหม่ (เช่นหน่วยความจำ, descriptors)

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

  • สลับสถานะของสำเนาท้องถิ่นrhsและ*thisเป็นมักจะค่อนข้างง่ายที่จะทำโดยไม่ต้องมีศักยภาพความล้มเหลว / ข้อยกเว้นให้สำเนาไม่จำเป็นต้องรัฐใด ๆ หลังจากนั้น (เพียงแค่ต้องการพอดีรัฐสำหรับ destructor ในการทำงานให้มากที่สุดเท่าสำหรับวัตถุที่ถูกย้ายจากใน> = C ++ 11)

ควรใช้เมื่อใด (ปัญหาใดบ้างที่แก้ไข[/ สร้าง] ?)

  • เมื่อคุณต้องการมอบหมายให้คัดค้านไม่ได้รับผลกระทบจากการมอบหมายที่ส่งข้อยกเว้นสมมติว่าคุณมีหรือสามารถเขียนswapด้วยการรับประกันข้อยกเว้นที่แข็งแกร่งและในอุดมคติที่ไม่สามารถล้มเหลว / throw.. †

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

    • การมอบหมายด้วยตนเองทำได้โดยการคัดลอกและสลับเพื่อหลีกเลี่ยงกรณีขอบที่มองข้าม‡

  • เมื่อใดก็ตามที่การปรับประสิทธิภาพหรือการใช้ทรัพยากรที่สูงขึ้นในไม่ช้านี้สร้างขึ้นโดยมีวัตถุชั่วคราวพิเศษระหว่างการกำหนดไม่สำคัญต่อแอปพลิเคชันของคุณ ⁂

swapขว้างปา: มันเป็นไปได้ที่จะเชื่อถือข้อมูลสมาชิกแลกเปลี่ยนว่าวัตถุติดตามโดยตัวชี้ แต่ไม่ใช่ตัวชี้ข้อมูลสมาชิกที่ไม่ได้มีการแลกเปลี่ยนโยนฟรีหรือที่แลกเปลี่ยนจะต้องมีการดำเนินการเป็นX tmp = lhs; lhs = rhs; rhs = tmp;และคัดลอกการก่อสร้างหรือการกำหนด อาจมีการโยน แต่ก็ยังมีโอกาสที่จะล้มเหลวในการทิ้งข้อมูลสมาชิกบางส่วนที่เปลี่ยนและอื่น ๆ ไม่ได้ ความเป็นไปได้นี้ใช้ได้แม้กับ C ++ 03std::stringในขณะที่เจมส์แสดงความคิดเห็นกับคำตอบอื่น:

@wilhelmtell: ใน C ++ 03 ไม่มีการกล่าวถึงข้อยกเว้นที่อาจเกิดขึ้นจาก std :: string :: swap (ซึ่งถูกเรียกโดย std :: swap) ใน C ++ 0x, std :: string :: swap คือ noexcept และต้องไม่ส่งข้อยกเว้น - James McNellis 22 ธ.ค. 53 เวลา 15:24 น


implementation การดำเนินการของผู้ประกอบการที่ได้รับมอบหมายซึ่งดูเหมือนว่ามีเหตุผลเมื่อกำหนดจากวัตถุที่แตกต่างสามารถล้มเหลวในการกำหนดตนเองได้อย่างง่ายดาย แม้ว่ามันอาจดูเหมือนเป็นไปไม่ได้ที่รหัสไคลเอนต์จะพยายามกำหนดตนเอง แต่มันสามารถเกิดขึ้นได้ค่อนข้างง่ายในระหว่างการดำเนินงานอัลโกบนคอนเทนเนอร์ด้วยx = f(x);รหัสที่f(อาจมีเฉพาะบาง#ifdefสาขา) แมโคร ala #define f(x) xหรือฟังก์ชันที่ส่งคืนการอ้างอิงถึงxหรือ (น่าจะไม่มีประสิทธิภาพ แต่รัดกุม) รหัสเหมือนx = c1 ? x * 2 : c2 ? x / 2 : x;) ตัวอย่างเช่น:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

เมื่อวันที่ตัวเองได้รับมอบหมายของโค้ดด้านบนลบx.p_;จุดp_ในเขตกองจัดสรรใหม่แล้วพยายามที่จะอ่านuninitialisedข้อมูลนั้น (พฤติกรรมที่ไม่ได้กำหนด) หากที่ไม่ได้ทำอะไรแปลกเกินไป, copyความพยายามในการที่มีการกำหนดตัวเองทุก just- ถูกทำลาย 'T'!


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

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

ที่นี่การเขียนด้วยมือClient::operator=อาจตรวจสอบว่า*thisเชื่อมต่อกับเซิร์ฟเวอร์เดียวกันกับrhs(อาจส่งรหัส "รีเซ็ต" หากมีประโยชน์) ในขณะที่วิธีการคัดลอกและสลับจะเรียกใช้ตัวคัดลอกที่จะเขียนเพื่อเปิด การเชื่อมต่อซ็อกเก็ตที่แตกต่างจากนั้นปิดอันเดิม ไม่เพียงแค่นั้นอาจหมายถึงการมีปฏิสัมพันธ์เครือข่ายระยะไกลแทนการคัดลอกตัวแปรในกระบวนการอย่างง่าย แต่ก็สามารถเรียกใช้ไคลเอนต์หรือเซิร์ฟเวอร์ข้อ จำกัด ในทรัพยากรซ็อกเก็ตหรือการเชื่อมต่อ (แน่นอนว่าคลาสนี้มีอินเทอร์เฟซที่น่ากลัว แต่นั่นเป็นอีกเรื่อง ;-P)


4
ที่กล่าวว่าการเชื่อมต่อซ็อกเก็ตเป็นเพียงตัวอย่าง - หลักการเดียวกันนี้ใช้กับการเริ่มต้นที่มีราคาแพงเช่นการตรวจสอบฮาร์ดแวร์ / การเริ่มต้น / การสอบเทียบการสร้างกลุ่มของเธรดหรือตัวเลขสุ่มงานเข้ารหัสบางอย่างแคชสแกนระบบไฟล์ฐานข้อมูล การเชื่อมต่อ ฯลฯ ..
Tony Delroy

มีอีกหนึ่ง (ใหญ่) แย้ง จากข้อมูลจำเพาะทางเทคนิคในปัจจุบันวัตถุจะไม่มีตัวดำเนินการย้ายที่ได้รับมอบหมาย! หากใช้ในภายหลังในฐานะสมาชิกของคลาสคลาสใหม่จะไม่สร้าง ctor แบบอัตโนมัติ! ที่มา: youtu.be/mYrbivnruYw?t=43m14s
user362515

3
ปัญหาหลักของตัวดำเนินการการคัดลอกClientคือไม่ได้รับอนุญาต
sbi

ในตัวอย่างลูกค้าคลาสควรทำให้ไม่สามารถคัดลอกได้
John Z. Li

25

คำตอบนี้เป็นเหมือนการเพิ่มและการแก้ไขคำตอบด้านบนเล็กน้อย

ในบางเวอร์ชันของ Visual Studio (และอาจเป็นคอมไพเลอร์อื่น ๆ ) มีข้อผิดพลาดที่น่ารำคาญจริงๆและไม่สมเหตุสมผล ดังนั้นหากคุณประกาศ / กำหนดswapฟังก์ชั่นของคุณเช่นนี้:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... คอมไพเลอร์จะตะโกนใส่คุณเมื่อคุณเรียกใช้swapฟังก์ชัน:

ป้อนคำอธิบายรูปภาพที่นี่

สิ่งนี้เกี่ยวข้องกับfriendฟังก์ชันที่เรียกใช้และthisวัตถุถูกส่งผ่านเป็นพารามิเตอร์


วิธีนี้คือการไม่ใช้friendคำหลักและกำหนดswapฟังก์ชัน:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

เวลานี้คุณสามารถโทรswapและส่งต่อได้otherทำให้คอมไพเลอร์มีความสุข:

ป้อนคำอธิบายรูปภาพที่นี่


ท้ายที่สุดคุณไม่จำเป็นต้องใช้friendฟังก์ชั่นในการสลับ 2 วัตถุ มันสมเหตุสมผลมากที่จะทำให้swapฟังก์ชั่นสมาชิกที่มีหนึ่งotherวัตถุเป็นพารามิเตอร์

คุณมีสิทธิ์เข้าถึงthisวัตถุอยู่แล้วดังนั้นจึงผ่านเป็นพารามิเตอร์ซ้ำซ้อนทางเทคนิค


1
@GManNickG dropbox.com/s/o1mitwcpxmawcot/example.cpp dropbox.com/s/jrjrn5dh1zez5vy/Untitled.jpg นี่เป็นเวอร์ชั่นที่เรียบง่าย ดูเหมือนว่าข้อผิดพลาดจะเกิดขึ้นทุกครั้งที่มีfriendการเรียกใช้ฟังก์ชันพร้อม*thisพารามิเตอร์
Oleksiy

1
@GManNickG อย่างที่ฉันบอกไปมันเป็นข้อผิดพลาดและอาจใช้ได้ผลดีกับคนอื่น ฉันแค่ต้องการช่วยคนที่อาจมีปัญหาเช่นเดียวกับฉัน ฉันพยายามนี้มีทั้ง Visual Studio 2012 Express และ 2,013 ตัวอย่างและสิ่งเดียวที่ทำให้มันหายไปก็คือการปรับเปลี่ยนของฉัน
Oleksiy

8
@GManNickG มันจะไม่พอดีกับความคิดเห็นที่มีภาพและรหัสตัวอย่าง และก็โอเคถ้ามีคนลงคะแนนฉันแน่ใจว่ามีใครบางคนอยู่ที่นั่นซึ่งได้รับข้อผิดพลาดเดียวกัน ข้อมูลในโพสต์นี้อาจเป็นสิ่งที่พวกเขาต้องการ
Oleksiy

14
โปรดทราบว่านี่เป็นเพียงข้อบกพร่องในการเน้นรหัส IDE (IntelliSense) ... มันจะรวบรวมได้ดีโดยไม่มีคำเตือน / ข้อผิดพลาด
Amro

3
โปรดรายงานข้อผิดพลาด VS ที่นี่หากคุณยังไม่ได้ดำเนินการ (และหากยังไม่ได้รับการแก้ไข) connect.microsoft.com/VisualStudio
Matt

15

ฉันต้องการเพิ่มคำเตือนเมื่อคุณกำลังจัดการกับคอนเทนเนอร์ C ++ แบบ 11 ตัวรู้การจัดสรร การแลกเปลี่ยนและการมอบหมายมีความหมายที่แตกต่างกันอย่างละเอียด

เพื่อความเป็นรูปธรรมขอให้เราพิจารณาคอนเทนเนอร์std::vector<T, A>ซึ่งAเป็นประเภทตัวจัดสรรแบบไร้รัฐและเราจะเปรียบเทียบฟังก์ชันต่อไปนี้:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

วัตถุประสงค์ของทั้งสองฟังก์ชั่นfsและfmคือการให้aรัฐที่bมีในขั้นต้น อย่างไรก็ตามมีคำถามที่ซ่อนอยู่: เกิดอะไรขึ้นถ้าa.get_allocator() != b.get_allocator()? คำตอบคือ: ขึ้นอยู่กับ มาเขียนAT = std::allocator_traits<A>กัน

  • หากAT::propagate_on_container_move_assignmentเป็นstd::true_typeเช่นนั้นfmให้มอบหมายการจัดสรรใหม่aด้วยค่าb.get_allocator()มิฉะนั้นจะไม่ใช้และaยังคงใช้ตัวจัดสรรดั้งเดิมต่อไป ในกรณีนั้นองค์ประกอบของข้อมูลจะต้องสลับเป็นรายบุคคลเนื่องจากการจัดเก็บaและbไม่เข้ากัน

  • ถ้าAT::propagate_on_container_swapเป็นstd::true_typeเช่นนั้นfsแลกเปลี่ยนข้อมูลและตัวจัดสรรในแบบที่คาดไว้

  • ถ้าAT::propagate_on_container_swapเป็นstd::false_typeเช่นนั้นเราต้องตรวจสอบแบบไดนามิก

    • ถ้าa.get_allocator() == b.get_allocator()จากนั้นทั้งสองคอนเทนเนอร์ใช้ที่เก็บข้อมูลที่เข้ากันได้และการแลกเปลี่ยนจะดำเนินต่อไปตามปกติ
    • อย่างไรก็ตามหากa.get_allocator() != b.get_allocator()โปรแกรมนั้นมีพฤติกรรมที่ไม่ได้กำหนด (cf. [container.requirements.general / 8]

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

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