ซีแมนทิกส์ย้ายคืออะไร?


1701

ฉันเพิ่งเสร็จสิ้นการฟังวิทยุวิศวกรรมซอฟต์แวร์สัมภาษณ์พอดคาสต์กับสกอตต์เมเยอร์สเกี่ยวกับภาษา C ++ 0x คุณสมบัติใหม่ส่วนใหญ่มีเหตุผลสำหรับฉันและฉันรู้สึกตื่นเต้นจริง ๆ กับ C ++ 0x ตอนนี้ยกเว้นหนึ่งคุณลักษณะ ฉันยังไม่เข้าใจความหมาย ... มันคืออะไรกันแน่?


20
ฉันพบ [บทความในบล็อกของ Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/ … ) เกี่ยวกับ lvalues ​​และ rvalues ​​ใน C และ C ++ เป็นข้อมูลที่ค่อนข้างดี นอกจากนี้เขายังกล่าวถึงการอ้างอิงค่าใน C ++ 11 และแนะนำพวกเขาด้วยตัวอย่างเล็ก ๆ
นิลส์


19
ทุก ๆ ปีฉันสงสัยว่าซีแมนติกส์การย้าย "ใหม่" ใน C ++ นั้นเกี่ยวกับอะไรฉัน google และไปที่หน้านี้ ฉันอ่านคำตอบสมองของฉันก็ดับลง ฉันกลับไปที่ C และลืมทุกอย่าง! ฉันถกเถียงกัน
ท้องฟ้า

7
@sky พิจารณา std :: vector <> ... บางแห่งในนั้นมีตัวชี้ไปยังอาร์เรย์บนฮีป หากคุณคัดลอกวัตถุนี้จะต้องมีการจัดสรรบัฟเฟอร์ใหม่และต้องคัดลอกข้อมูลจากบัฟเฟอร์ไปยังบัฟเฟอร์ใหม่ มีสถานการณ์ใดบ้างไหมที่จะเป็นการดีที่จะขโมยตัวชี้? คำตอบคือใช่เมื่อคอมไพเลอร์รู้ว่าวัตถุนั้นชั่วคราว ซีแมนทิคส์การย้ายช่วยให้คุณสามารถกำหนดวิธีการที่คลาสของคุณสามารถย้ายออกและวางในวัตถุที่แตกต่างเมื่อคอมไพเลอร์รู้ว่าวัตถุที่คุณกำลังจะย้ายออกไปกำลังจะหายไป
dicroce

การอ้างอิงเดียวที่ฉันเข้าใจ: learncpp.com/cpp-tutorial/ …นั่นคือเหตุผลดั้งเดิมของซีแมนทิกส์การย้ายมาจากตัวชี้สมาร์ท
jw_

คำตอบ:


2480

ฉันคิดว่ามันง่ายที่สุดในการเข้าใจการย้ายความหมายด้วยโค้ดตัวอย่าง เริ่มต้นด้วยคลาสสตริงที่ง่ายมากซึ่งเก็บตัวชี้ไปยังบล็อกหน่วยความจำที่จัดสรรฮีป:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

เนื่องจากเราเลือกจัดการหน่วยความจำด้วยตนเองเราจึงต้องปฏิบัติตามกฎสามข้อ ฉันจะเลื่อนการเขียนผู้ดำเนินการที่ได้รับมอบหมายและใช้ destructor และตัวสร้างการคัดลอกเฉพาะตอนนี้

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

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

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

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

อาร์กิวเมนต์ในบรรทัดที่ 2 และ 3 ไม่ใช่ lvalues ​​แต่ rvalues ​​เนื่องจากวัตถุสตริงต้นแบบไม่มีชื่อดังนั้นไคลเอ็นต์จึงไม่มีวิธีตรวจสอบอีกครั้งในเวลาต่อมา rvalues ​​แสดงถึงวัตถุชั่วคราวที่ถูกทำลายในเซมิโคลอนถัดไป (เพื่อความแม่นยำมากขึ้น: ในตอนท้ายของนิพจน์เต็มรูปแบบที่มีค่า rvalue) สิ่งนี้มีความสำคัญเนื่องจากในระหว่างการเริ่มต้นbและcเราสามารถทำสิ่งที่เราต้องการด้วยสตริงที่มาและลูกค้าไม่สามารถบอกความแตกต่าง !

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

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

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

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

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

นั่นไงเหรอ? "การอ้างอิงค่า rvalue อยู่ที่ไหน" คุณอาจถาม "เราไม่ต้องการที่นี่!" คือคำตอบของฉัน :)

โปรดทราบว่าเราส่งพารามิเตอร์that ตามค่าดังนั้นthatจะต้องเริ่มต้นเช่นเดียวกับวัตถุสตริงอื่น ๆ ว่าวิธีการที่เป็นthatไปได้ที่จะเริ่มต้น? ในสมัยก่อนของC ++ 98คำตอบน่าจะเป็น "ตัวสร้างสำเนา" ใน C ++ 0x คอมไพเลอร์เลือกระหว่างตัวสร้างการคัดลอกและตัวสร้างการย้ายขึ้นอยู่กับว่าอาร์กิวเมนต์ของตัวดำเนินการกำหนดค่าเป็น lvalue หรือ rvalue

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

แต่ถ้าคุณบอกว่าa = x + yตัวสร้างการย้ายจะเริ่มต้นthat(เนื่องจากนิพจน์x + yนั้นเป็นค่า rvalue) ดังนั้นจึงไม่มีการคัดลอกแบบลึกที่เกี่ยวข้อง thatยังคงเป็นวัตถุอิสระจากการโต้แย้ง แต่การก่อสร้างนั้นไม่สำคัญเนื่องจากข้อมูลฮีปไม่จำเป็นต้องคัดลอกเพียงแค่ย้าย ไม่จำเป็นต้องคัดลอกเพราะx + yเป็นค่า rvalue และอีกครั้งมันก็โอเคที่จะย้ายจากวัตถุสตริงที่แสดงด้วย rvalues

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

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


40
@ แต่ถ้า ctor ของฉันมีค่า rvalue ซึ่งไม่สามารถใช้ได้ในภายหลังทำไมฉันถึงต้องรำคาญที่จะปล่อยให้มันอยู่ในสภาพที่ปลอดภัย / สม่ำเสมอ แทนที่จะตั้งค่า that.data = 0 ทำไมไม่ปล่อยไว้
einpoklum

70
@einpoklum เพราะถ้าไม่มีthat.data = 0ตัวละครจะถูกทำลายเร็วเกินไป (เมื่อตายชั่วคราว) และยังเป็นสองเท่า คุณต้องการขโมยข้อมูลไม่แชร์!
fredoverflow

19
@einpoklum destructor ที่กำหนดเป็นประจำยังคงทำงานดังนั้นคุณต้องตรวจสอบให้แน่ใจว่าสถานะหลังการย้ายวัตถุต้นทางไม่ทำให้เกิดความผิดพลาด ดีกว่าคุณควรตรวจสอบให้แน่ใจว่าวัตถุต้นฉบับนั้นยังสามารถเป็นผู้รับการมอบหมายหรือการเขียนอื่น ๆ ได้
CTMacUser

12
@pranitkothari ใช่วัตถุทั้งหมดจะต้องถูกทำลายแม้จะย้ายจากวัตถุ และเนื่องจากเราไม่ต้องการลบอาร์เรย์ถ่านเมื่อเกิดขึ้นเราจึงต้องตั้งค่าพอยน์เตอร์ให้เป็นโมฆะ
fredoverflow

7
@ Virus721 delete[]บน nullptr ถูกกำหนดโดยมาตรฐาน C ++ ให้เป็นแบบไม่ใช้งาน
fredoverflow

1057

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

สเตฟานต. Lavavej ใช้เวลาในการแสดงความคิดเห็นที่มีค่า ขอบคุณมากสเตฟาน!

บทนำ

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

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

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. การใช้ประเภท "ย้ายเท่านั้น" ที่ปลอดภัย นั่นคือชนิดที่การทำสำเนาไม่สมเหตุสมผล แต่การย้ายทำได้ ตัวอย่างเช่นการล็อกตัวจัดการไฟล์และพอยน์เตอร์อัจฉริยะที่มีซีแมนทิกส์เฉพาะ หมายเหตุ: คำตอบนี้กล่าวถึงstd::auto_ptrเทมเพลตไลบรารีมาตรฐาน C ++ 98 ที่คัดค้านซึ่งถูกแทนที่ด้วยstd::unique_ptrใน C ++ 11 โปรแกรมเมอร์ C ++ ระดับกลางอาจมีความคุ้นเคยอย่างน้อยstd::auto_ptrและเนื่องจาก "ความหมายของการย้าย" แสดงขึ้นดูเหมือนว่าเป็นจุดเริ่มต้นที่ดีสำหรับการพูดคุยความหมายของการย้ายใน C ++ 11 YMMV

การเคลื่อนไหวคืออะไร?

c ++ 98 std::auto_ptr<T>มาตรฐานมีห้องสมุดชี้สมาร์ทที่มีความหมายเป็นเจ้าของที่ไม่ซ้ำกันเรียกว่า ในกรณีที่คุณไม่คุ้นเคยauto_ptrจุดประสงค์ของมันคือเพื่อรับประกันว่าวัตถุที่จัดสรรแบบไดนามิกจะถูกปล่อยออกมาเสมอแม้ในกรณีที่มีข้อยกเว้น:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

สิ่งผิดปกติเกี่ยวกับauto_ptrคือพฤติกรรม "คัดลอก":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

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

ในการย้ายวัตถุหมายถึงการถ่ายโอนความเป็นเจ้าของทรัพยากรบางอย่างที่จัดการไปยังวัตถุอื่น

ตัวสร้างสำเนาของauto_ptrอาจมีลักษณะเช่นนี้ (ค่อนข้างง่าย):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

การเคลื่อนไหวที่เป็นอันตรายและไม่เป็นอันตราย

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

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

แต่auto_ptrไม่อันตรายเสมอไป ฟังก์ชั่นจากโรงงานเป็นกรณีการใช้งานที่สมบูรณ์แบบสำหรับauto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

สังเกตว่าทั้งสองตัวอย่างทำตามรูปแบบวากยสัมพันธ์เดียวกันอย่างไร:

auto_ptr<Shape> variable(expression);
double area = expression->area();

และหนึ่งในนั้นก็เรียกใช้พฤติกรรมที่ไม่ได้กำหนดในขณะที่อีกคนหนึ่งไม่ได้ ดังนั้นความแตกต่างระหว่างการแสดงออกaและmake_triangle()คืออะไร? พวกเขาทั้งสองประเภทเดียวกันไม่ได้เหรอ? อันที่จริงพวกเขามี แต่พวกเขามีความแตกต่างกันหมวดมูลค่า

หมวดหมู่ค่า

เห็นได้ชัดว่าจะต้องมีความแตกต่างอย่างลึกซึ้งระหว่างการแสดงออกaซึ่งหมายถึงauto_ptrตัวแปรและการแสดงออกmake_triangle()ซึ่งหมายถึงการเรียกใช้ฟังก์ชั่นที่ส่งกลับauto_ptrค่าโดยการสร้างauto_ptrวัตถุชั่วคราวสดทุกครั้งที่มันถูกเรียก aเป็นตัวอย่างของการเป็นนักการlvalueในขณะที่make_triangle()เป็นตัวอย่างของการเป็นนักการrvalue

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

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

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

rvalue ของประเภทชั้นเรียนคือการแสดงออกที่มีการประเมินผลสร้างวัตถุชั่วคราว ภายใต้สถานการณ์ปกติไม่มีนิพจน์อื่นภายในขอบเขตเดียวกันแสดงถึงวัตถุชั่วคราวเดียวกัน

การอ้างอิงค่า

ตอนนี้เราเข้าใจว่าการเคลื่อนย้ายจาก lvalues ​​อาจเป็นอันตราย แต่การย้ายจาก rvalues ​​นั้นไม่เป็นอันตราย หาก C ++ มีภาษารองรับเพื่อแยกความแตกต่างของข้อโต้แย้ง lvalue จากข้อโต้แย้ง rvalue เราสามารถห้ามการย้ายจาก lvalues ​​อย่างสมบูรณ์หรืออย่างน้อยก็ทำให้การย้ายจาก lvalues ชัดเจนที่ไซต์การโทรเพื่อให้เราไม่ย้ายโดยไม่ตั้งใจอีกต่อไป

คำตอบของ C ++ 11 สำหรับปัญหานี้คือการอ้างอิงที่คุ้มค่า การอ้างอิง rvalue เป็นชนิดใหม่ของการอ้างอิงที่ผูกเท่านั้นที่จะ rvalues X&&และไวยากรณ์คือ อ้างอิงเก่าที่ดีX&เป็นที่รู้จักกันตอนนี้เป็นอ้างอิง lvalue (หมายเหตุว่าX&&เป็นไม่ได้มีการอ้างอิงถึงการอ้างอิง; ไม่มีสิ่งดังกล่าวใน C ++.)

ถ้าเราโยนconstลงไปมิกซ์เรามีการอ้างอิงสี่แบบที่ต่างกัน ประเภทของการแสดงออกประเภทใดXพวกเขาสามารถผูกกับ?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

const X&&ในทางปฏิบัติคุณสามารถลืมเกี่ยวกับ การ จำกัด ให้อ่านจากค่า rvalues ​​นั้นไม่มีประโยชน์มากนัก

การอ้างอิง rvalue เป็นการอ้างอิงX&&ชนิดใหม่ที่ผูกกับค่า rvalue เท่านั้น

การแปลงโดยนัย

การอ้างอิงที่คุ้มค่าผ่านหลายเวอร์ชัน ตั้งแต่รุ่น 2.1 การอ้างอิง rvalue X&&ยังผูกกับทุกประเภทมูลค่าของประเภทที่แตกต่างกันYให้มีการแปลงนัยจากการY XในกรณีดังXกล่าวจะมีการสร้างชนิดชั่วคราวและการอ้างอิง rvalue จะเชื่อมโยงกับชั่วคราว:

void some_function(std::string&& r);

some_function("hello world");

ในตัวอย่างข้างต้น"hello world"เป็น lvalue const char[12]ประเภท นับตั้งแต่มีการแปลงนัยจากconst char[12]ผ่านconst char*ไปstd::stringเป็นชั่วคราวของประเภทstd::stringจะถูกสร้างขึ้นและrถูกผูกไว้ชั่วคราวที่ นี่เป็นหนึ่งในกรณีที่ความแตกต่างระหว่าง rvalues ​​(นิพจน์) และ tempages (วัตถุ) นั้นเบลอเล็กน้อย

ย้ายตัวสร้าง

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

ใน C ++ 11 std::auto_ptr<T>ถูกแทนที่ด้วยstd::unique_ptr<T>ซึ่งใช้ประโยชน์จากการอ้างอิง rvalue unique_ptrฉันจะมีการพัฒนาและหารือฉบับง่าย อันดับแรกเราใส่แค็ปซูลตัวชี้แบบดิบและโอเวอร์โหลดตัวดำเนินการ->และ*ดังนั้นคลาสของเราจึงรู้สึกเหมือนตัวชี้:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

ตัวสร้างใช้ความเป็นเจ้าของของวัตถุและ destructor ลบมัน:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

ตอนนี้ส่วนที่น่าสนใจผู้สร้างย้าย:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

ตัวสร้างการย้ายนี้ทำสิ่งที่ตัวauto_ptrสร้างการคัดลอกทำ แต่สามารถให้ค่า rvalues ​​ได้เท่านั้น:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

บรรทัดที่สองไม่สามารถคอมไพล์ได้เนื่องจากaเป็น lvalue แต่พารามิเตอร์unique_ptr&& sourceสามารถผูกกับ rvalues ​​เท่านั้น ตรงนี้เป็นสิ่งที่เราต้องการ การเคลื่อนไหวที่อันตรายไม่ควรกระทำโดยปริยาย บรรทัดที่สามคอมไพล์ได้ดีเพราะmake_triangle()เป็นค่า rvalue cนวกรรมิกย้ายจะโอนกรรมสิทธิ์จากการชั่วคราว นี่คือสิ่งที่เราต้องการ

ตัวสร้างการเคลื่อนย้ายถ่ายโอนความเป็นเจ้าของของทรัพยากรที่มีการจัดการลงในวัตถุปัจจุบัน

ย้ายผู้ประกอบการที่ได้รับมอบหมาย

ชิ้นสุดท้ายที่หายไปคือผู้ประกอบการที่ได้รับมอบหมายย้าย หน้าที่คือการปล่อยทรัพยากรเก่าและรับทรัพยากรใหม่จากการโต้แย้ง:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

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

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

ตอนนี้sourceเป็นตัวแปรประเภทunique_ptrมันจะเริ่มต้นได้โดยตัวสร้างการย้าย; นั่นคืออาร์กิวเมนต์จะถูกย้ายไปยังพารามิเตอร์ อาร์กิวเมนต์ยังจำเป็นต้องเป็นค่า rvalue เนื่องจากตัวสร้างการย้ายเองมีพารามิเตอร์อ้างอิง rvalue เมื่อการควบคุมการไหลถึงรั้งปิดoperator=, sourceไปจากขอบเขตปล่อยทรัพยากรเก่าโดยอัตโนมัติ

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

ย้ายจากค่า lvalues

บางครั้งเราต้องการย้ายจาก lvalues นั่นคือบางครั้งเราต้องการให้คอมไพเลอร์รักษา lvalue ราวกับว่าเป็น rvalue ดังนั้นจึงสามารถเรียกใช้ตัวสร้างการย้ายแม้ว่ามันอาจจะไม่ปลอดภัยก็ตาม เพื่อจุดประสงค์นี้, C ++ 11 ข้อเสนอฟังก์ชั่นมาตรฐานห้องสมุดแม่แบบที่เรียกว่าภายในส่วนหัวstd::move <utility>ชื่อนี้ค่อนข้างโชคร้ายเพราะstd::moveเพียงแค่ใช้ค่า lvalue กับค่า rvalue มันไม่ได้ย้ายอะไรด้วยตัวเอง มันทำให้เคลื่อนไหวได้ บางทีมันควรจะมีชื่อstd::cast_to_rvalueหรือstd::enable_moveแต่เราติดอยู่กับชื่อตอนนี้

นี่คือวิธีที่คุณย้ายจาก lvalue อย่างชัดเจน:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

โปรดทราบว่าหลังจากบรรทัดที่สามaไม่มีเจ้าของรูปสามเหลี่ยมอีกต่อไป ก็ไม่เป็นไรเพราะโดยชัดเจนเขียนstd::move(a)เราทำให้ความตั้งใจของเราที่ชัดเจน: "คอนสตรัคที่รักทำสิ่งที่คุณต้องการด้วยaเพื่อที่จะเริ่มต้นcผมไม่สนใจเกี่ยวกับaอีกต่อไปรู้สึกอิสระที่จะมีวิธีของคุณด้วย. a."

std::move(some_lvalue) ปลดล็อก lvalue ให้กับ rvalue ดังนั้นจึงทำให้สามารถเคลื่อนที่ได้ในภายหลัง

Xvalues

โปรดทราบว่าแม้ว่าจะstd::move(a)เป็นค่า rvalue การประเมินค่าจะไม่สร้างวัตถุชั่วคราว ปริศนานี้บังคับให้คณะกรรมการแนะนำหมวดหมู่คุณค่าที่สาม สิ่งที่สามารถผูกมัดกับการอ้างอิงค่า rvalue แม้ว่าจะไม่ใช่ rvalue ในความหมายดั้งเดิมเรียกว่าxvalue (eXpiring ตามตัวอักษร) ค่า rvalues ​​ดั้งเดิมถูกเปลี่ยนชื่อเป็นprvalues (ค่า r Pure แบบบริสุทธิ์)

ทั้ง prvalues ​​และ xvalues ​​เป็น rvalues Xvalues และ lvalues มีทั้งglvalues (lvalues ทั่วไป) ความสัมพันธ์นั้นง่ายต่อการเข้าใจด้วยไดอะแกรม:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

โปรดทราบว่าเฉพาะค่า xvalues ​​ที่เป็นจริงเท่านั้น ส่วนที่เหลือเป็นเพียงการเปลี่ยนชื่อและการจัดกลุ่ม

c ++ 98 rvalues ​​เรียกว่า prvalues ​​ใน C ++ 11 ทางใจแทนที่ "rvalue" ที่เกิดขึ้นทั้งหมดในย่อหน้าก่อนหน้าด้วย "prvalue"

ย้ายออกจากฟังก์ชั่น

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

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

บางทีน่าแปลกใจที่วัตถุอัตโนมัติ (ตัวแปรท้องถิ่นที่ไม่ได้ประกาศไว้static) สามารถย้ายออกจากฟังก์ชันโดยปริยายได้ :

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

ตัวสร้างการย้ายมาทำไมยอมรับ lvalue resultเป็นอาร์กิวเมนต์? ขอบเขตของresultกำลังจะสิ้นสุดและจะถูกทำลายระหว่างการคลายสแต็ก ไม่มีใครสามารถบ่นได้หลังจากนั้นresultมีการเปลี่ยนแปลงอย่างใด; เมื่อโฟลว์คอนโทรลกลับมาที่ผู้โทรresultไม่มีอยู่อีกต่อไป! สำหรับเหตุผลที่ C ++ 11 std::moveมีกฎพิเศษที่ช่วยให้กลับวัตถุอัตโนมัติจากฟังก์ชั่นโดยไม่ต้องเขียน ในความเป็นจริงคุณไม่ควรใช้std::moveเพื่อย้ายวัตถุอัตโนมัติออกจากฟังก์ชั่นเนื่องจากจะเป็นการยับยั้ง "การปรับค่าตอบแทนที่มีชื่อ" (NRVO)

อย่าใช้std::moveเพื่อย้ายวัตถุอัตโนมัติออกจากฟังก์ชั่น

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

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

ไม่ส่งคืนวัตถุอัตโนมัติโดยการอ้างอิงค่า การเคลื่อนย้ายทำได้โดยตัวสร้างการย้ายเท่านั้นไม่ใช่โดยstd::moveและไม่เพียง แต่ผูกค่า rvalue กับการอ้างอิง rvalue

ย้ายเข้าเป็นสมาชิก

ไม่ช้าก็เร็วคุณจะต้องเขียนโค้ดดังนี้:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

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

การอ้างอิง rvalue ที่มีชื่อคือ lvalue เช่นเดียวกับตัวแปรอื่น ๆ

วิธีแก้ปัญหาคือการเปิดใช้งานการย้ายด้วยตนเอง:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

คุณสามารถยืนยันว่าจะไม่ใช้อีกต่อไปหลังจากที่เริ่มต้นของparameter memberเหตุใดจึงไม่มีกฎพิเศษในการแทรกอย่างเงียบ ๆstd::moveเช่นเดียวกับค่าส่งคืน อาจเป็นเพราะมันจะเป็นภาระมากเกินไปในการใช้งานคอมไพเลอร์ ตัวอย่างเช่นเกิดอะไรขึ้นถ้าเนื้อความของตัวสร้างอยู่ในหน่วยการแปลอื่น ในทางกลับกันกฎคืนค่าจะต้องตรวจสอบตารางสัญลักษณ์เพื่อพิจารณาว่าตัวระบุหลังจากreturnคำหลักแสดงถึงวัตถุอัตโนมัติหรือไม่

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

ฟังก์ชั่นสมาชิกพิเศษ

C ++ 98 ประกาศหน้าที่พิเศษสมาชิกสามฟังก์ชันตามความต้องการนั่นคือเมื่อพวกเขาต้องการที่ไหนสักแห่ง: ตัวสร้างการคัดลอกตัวดำเนินการกำหนดค่าการคัดลอกและ destructor

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

การอ้างอิงที่คุ้มค่าผ่านหลายเวอร์ชัน ตั้งแต่เวอร์ชัน 3.0, C ++ 11 ประกาศเพิ่มสมาชิกฟังก์ชันพิเศษสองตัวตามต้องการ: ตัวสร้างการย้ายและตัวดำเนินการกำหนดค่าการย้าย โปรดทราบว่าทั้ง VC10 และ VC11 นั้นไม่เป็นไปตามเวอร์ชัน 3.0 ดังนั้นคุณจะต้องใช้มันเอง

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

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

กฎเหล่านี้มีความหมายในทางปฏิบัติอย่างไร

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

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

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

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

การส่งต่อการอ้างอิง ( ก่อนหน้านี้รู้จักกันในนามการอ้างอิงสากล )

พิจารณาเทมเพลตฟังก์ชันต่อไปนี้:

template<typename T>
void foo(T&&);

คุณอาจคาดว่าT&&จะผูกกับค่าเพียงเพราะอย่างรวดเร็วก่อนดูเหมือนว่าการอ้างอิงค่า ตามที่ปรากฎว่าT&&ยังผูกกับ lvalues ​​ด้วย:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

ถ้าอาร์กิวเมนต์เป็น rvalue ประเภทX, Tจะสรุปได้ว่าจะเป็นXจึงหมายความว่าT&& X&&นี่คือสิ่งที่ทุกคนคาดหวัง แต่ถ้าอาร์กิวเมนต์เป็น lvalue ประเภทXเนื่องจากกฎพิเศษTจะสรุปได้ว่าจะเป็นX&ด้วยเหตุนี้จะหมายถึงสิ่งที่ต้องการT&& X& &&แต่เนื่องจากภาษา C ++ ยังคงมีความคิดของการอ้างอิงถึงการอ้างอิงไม่มีชนิดX& &&จะทรุดX&ลงไป สิ่งนี้อาจฟังดูสับสนและไร้ประโยชน์ในตอนแรก แต่การยุบอ้างอิงเป็นสิ่งจำเป็นสำหรับการส่งต่อที่สมบูรณ์แบบ (ซึ่งจะไม่มีการกล่าวถึงที่นี่)

T&& ไม่ใช่การอ้างอิงค่า แต่เป็นการส่งต่อการอ้างอิง นอกจากนี้ยังผูกกับ lvalues ​​ซึ่งในกรณีนี้TและT&&เป็นการอ้างอิง lvalue ทั้งคู่

หากคุณต้องการ จำกัด แม่แบบฟังก์ชั่นเพื่อให้ค่าคุณสามารถรวมSFINAEกับลักษณะประเภท:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

การดำเนินการย้าย

หลังจากที่คุณเข้าใจการอ้างอิงแบบยุบนี่คือวิธีstd::moveการใช้งาน

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

ตามที่คุณเห็นmoveยอมรับพารามิเตอร์ทุกชนิดด้วยการอ้างอิงการส่งต่อT&&และจะส่งคืนการอ้างอิง rvalue std::remove_reference<T>::typeโทร meta ฟังก์ชั่นเป็นสิ่งที่จำเป็นเพราะมิฉะนั้นสำหรับ lvalues ประเภทXประเภทผลตอบแทนจะเป็นซึ่งจะพังครืนลงมาX& && X&เนื่องจากtเป็น lvalue เสมอ (โปรดจำไว้ว่าการอ้างอิง rvalue ที่มีชื่อคือ lvalue) แต่เราต้องการผูกtกับการอ้างอิง rvalue เราจึงtต้องส่งกลับไปที่ประเภทการส่งคืนที่ถูกต้องอย่างชัดเจน การเรียกใช้ฟังก์ชันที่ส่งคืนการอ้างอิง rvalue นั้นคือ xvalue ตอนนี้คุณรู้แล้วว่าค่ามาจากไหน;)

การเรียกใช้ฟังก์ชันที่ส่งคืนการอ้างอิง rvalue เช่นstd::moveคือ xvalue

โปรดทราบว่าการส่งคืนโดยการอ้างอิง rvalue นั้นใช้ได้ในตัวอย่างนี้เนื่องจากtไม่ได้หมายถึงวัตถุอัตโนมัติ แต่แทนวัตถุที่ถูกส่งผ่านโดยผู้เรียกแทน



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

8
ฉันอยู่กับคุณจนถึง 'การอ้างอิงสากล' แต่มันก็เป็นนามธรรมเกินกว่าจะติดตามได้ การยุบอ้างอิง? ส่งต่อที่สมบูรณ์แบบ? คุณกำลังบอกว่าการอ้างอิงค่า rvalue กลายเป็นการอ้างอิงแบบสากลถ้าประเภทนั้นเป็นเทมเพลท? ฉันหวังว่าจะมีวิธีที่จะอธิบายเรื่องนี้เพื่อที่ฉันจะได้รู้ว่าฉันต้องเข้าใจมันหรือไม่! :)
Kylotan

8
กรุณาเขียนหนังสือตอนนี้ ... คำตอบนี้ทำให้ฉันมีเหตุผลที่จะเชื่อว่าถ้าคุณครอบคลุมมุมอื่น ๆ ของ C ++ ในลักษณะที่ชัดเจนเช่นนี้ผู้คนอีกหลายพันคนจะเข้าใจ
halivingston

12
@halivingston ขอบคุณมากสำหรับความคิดเห็นของคุณฉันขอขอบคุณจริงๆ ปัญหาเกี่ยวกับการเขียนหนังสือคือ: มันทำงานได้มากกว่าที่คุณจินตนาการได้ หากคุณต้องการขุดลึกลงไปใน C ++ 11 ขึ้นไปฉันขอแนะนำให้คุณซื้อ "Effective Modern C ++" โดย Scott Meyers
fredoverflow

77

ย้ายหมายจะขึ้นอยู่กับการอ้างอิง rvalue
ค่า rvalue เป็นวัตถุชั่วคราวซึ่งจะถูกทำลายเมื่อสิ้นสุดการแสดงออก ใน C ++ ปัจจุบัน rvalues ​​ผูกเฉพาะกับconstการอ้างอิง C ++ 1x จะอนุญาตconstการอ้างอิงที่ไม่ใช่ค่าที่ถูกสะกดคำT&&ซึ่งเป็นการอ้างอิงไปยังวัตถุ rvalue
เนื่องจากค่า rvalue กำลังจะตายในตอนท้ายของนิพจน์คุณสามารถขโมยข้อมูลได้ แทนที่จะคัดลอกลงในวัตถุอื่นคุณย้ายข้อมูลลงไป

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

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


1
โปรดทราบว่าควรเป็นthis->swap(std::move(rhs));เพราะการอ้างอิงชื่อ rvalue เป็น lvalues
wmamrak

นี้เป็นธรรมครับต่อ @ ความคิดเห็นของ Tacyt: rhsเป็นlvalueX::X(X&& rhs)ในบริบทของ คุณต้องโทรหาstd::move(rhs)ค่า rvalue แต่แบบนี้จะทำให้คำตอบนั้นเป็นคำตอบ
Asherah

ความหมายของการย้ายสำหรับประเภทที่ไม่มีตัวชี้คืออะไร ความหมายของการย้ายงานคัดลอก liky?
Gusev Slava

@Gusev: ฉันไม่รู้ว่าสิ่งที่คุณถาม
sbi

60

สมมติว่าคุณมีฟังก์ชั่นที่ส่งคืนวัตถุจำนวนมาก:

Matrix multiply(const Matrix &a, const Matrix &b);

เมื่อคุณเขียนโค้ดเช่นนี้:

Matrix r = multiply(a, b);

จากนั้นคอมไพเลอร์ C ++ สามัญจะสร้างวัตถุชั่วคราวสำหรับผลลัพธ์ของการmultiply()เรียกตัวสร้างการคัดลอกเพื่อเริ่มต้นrแล้วทำลายค่าส่งคืนชั่วคราว ความหมายของการย้ายใน C ++ 0x อนุญาตให้ "ตัวสร้างการย้าย" ถูกเรียกเพื่อเริ่มต้นrโดยการคัดลอกเนื้อหาแล้วละทิ้งค่าชั่วคราวโดยไม่ต้องทำลายมัน

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


2
ตัวสร้างการเคลื่อนย้ายและการคัดลอกตัวสร้างต่างกันอย่างไร
dicroce

1
@dicroce: พวกเขาต่างกันตามไวยากรณ์หนึ่งดูเหมือนเมทริกซ์ (const เมทริกซ์ & src) (คัดลอกคอนสตรัคเตอร์) และอื่น ๆ ดูเหมือนเมทริกซ์ (เมทริกซ์ && src) (ย้ายคอนสตรัค) ตรวจสอบคำตอบหลักของฉันเป็นตัวอย่างที่ดีกว่า
snk_kid

3
@dicroce: หนึ่งทำให้วัตถุว่างเปล่าและหนึ่งทำสำเนา หากข้อมูลที่เก็บไว้ในวัตถุมีขนาดใหญ่สำเนาอาจมีราคาแพง ตัวอย่างเช่น std :: vector
Billy ONeal

1
@ kunj2aan: มันขึ้นอยู่กับคอมไพเลอร์ของคุณฉันสงสัย คอมไพเลอร์สามารถสร้างวัตถุชั่วคราวภายในฟังก์ชั่นแล้วย้ายไปไว้ที่ค่าส่งคืนของผู้โทร หรือมันอาจจะสามารถสร้างวัตถุโดยตรงในค่าตอบแทนโดยไม่จำเป็นต้องใช้ตัวสร้างการย้าย
เกร็กฮิวกิล

2
@Jichao: นั่นคือการเพิ่มประสิทธิภาพที่เรียกว่า RVO ดูคำถามนี้สำหรับข้อมูลเพิ่มเติมเกี่ยวกับความแตกต่าง: stackoverflow.com/questions/5031778/…
Greg Hewgill

30

หากคุณสนใจในคำอธิบายเชิงลึกของการย้ายความหมายที่ดีจริงๆฉันขอแนะนำให้อ่านเอกสารต้นฉบับเกี่ยวกับพวกเขา"ข้อเสนอเพื่อเพิ่มความหมายของการย้ายความหมายกับภาษา C ++"

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


27

ความหมายของการย้ายคือการถ่ายโอนทรัพยากรแทนที่จะคัดลอกเมื่อไม่มีใครต้องการค่าแหล่งข้อมูลอีกต่อไป

ใน C ++ 03 วัตถุมักจะถูกคัดลอกเท่านั้นที่จะถูกทำลายหรือได้รับมอบหมายก่อนที่รหัสใด ๆ จะใช้ค่าอีกครั้ง ตัวอย่างเช่นเมื่อคุณกลับตามมูลค่าจากฟังก์ชั่น - เว้นแต่ RVO เริ่มเล่น - ค่าที่คุณส่งคืนจะถูกคัดลอกไปยังกรอบสแต็คของผู้โทรแล้วมันจะออกนอกขอบเขตและถูกทำลาย นี่เป็นเพียงหนึ่งในหลายตัวอย่าง: ดูการส่งต่อค่าเมื่อวัตถุต้นทางเป็นแบบชั่วคราวอัลกอริธึมแบบsortที่เพิ่งจัดเรียงรายการใหม่การจัดสรรใหม่vectorเมื่อcapacity()เกินค่า ฯลฯ

เมื่อคู่คัดลอก / ทำลายดังกล่าวมีราคาแพงมักจะเป็นเพราะวัตถุเป็นเจ้าของทรัพยากรเฮฟวี่เวทบางอย่าง ตัวอย่างเช่นvector<string>อาจเป็นเจ้าของบล็อกหน่วยความจำที่จัดสรรแบบไดนามิกที่มีอาร์เรย์ของstringวัตถุแต่ละรายการมีหน่วยความจำแบบไดนามิกของตัวเอง การคัดลอกวัตถุดังกล่าวมีค่าใช้จ่ายสูง: คุณต้องจัดสรรหน่วยความจำใหม่สำหรับบล็อกที่จัดสรรแบบไดนามิกแต่ละบล็อกในแหล่งที่มาและคัดลอกค่าทั้งหมดข้าม จากนั้นคุณต้องยกเลิกการจัดสรรหน่วยความจำทั้งหมดที่คุณเพิ่งคัดลอก อย่างไรก็ตามการย้ายวิธีการที่มีขนาดใหญ่vector<string>เพียงแค่คัดลอกพอยน์เตอร์สองสามตัว (ที่อ้างถึงบล็อกหน่วยความจำแบบไดนามิก) ไปยังปลายทางและ zeroing พวกมันออกในแหล่งที่มา


23

ในแง่ง่าย (ปฏิบัติ):

การคัดลอกวัตถุหมายถึงการคัดลอกสมาชิก "คงที่" และเรียกnewผู้ประกอบการหาวัตถุแบบไดนามิก ขวา?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

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

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

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

ตกลง แต่ถ้าฉันย้ายวัตถุวัตถุต้นทางจะไร้ประโยชน์ใช่ไหม? แน่นอน แต่ในบางสถานการณ์มันมีประโยชน์มาก สิ่งที่ชัดเจนที่สุดคือเมื่อฉันเรียกใช้ฟังก์ชันที่มีวัตถุที่ไม่ระบุตัวตน (วัตถุชั่วคราว, วัตถุ rvalue, ... , คุณสามารถเรียกมันด้วยชื่ออื่น):

void heavyFunction(HeavyType());

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

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

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

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

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

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

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

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

คำถามทั่วไปอื่น ๆ : อะไรคือความแตกต่างระหว่างA&&และconst A&&? แน่นอนในกรณีแรกคุณสามารถปรับเปลี่ยนวัตถุและในครั้งที่สองไม่ได้ แต่ความหมายในทางปฏิบัติ? ในกรณีที่สองคุณไม่สามารถแก้ไขได้ดังนั้นคุณจึงไม่มีวิธีที่จะทำให้วัตถุเป็นโมฆะ (ยกเว้นธงที่ไม่แน่นอนหรืออะไรทำนองนั้น) และไม่มีความแตกต่างในทางปฏิบัติกับตัวสร้างสำเนา

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

void some_function(A&& a)
{
   other_function(a);
}

วัตถุที่จะถูกคัดลอกไปพารามิเตอร์ที่แท้จริงของa other_functionหากคุณต้องการให้วัตถุนั้นaยังคงถูกใช้เป็นวัตถุชั่วคราวต่อไปคุณควรใช้std::moveฟังก์ชัน:

other_function(std::move(a));

ด้วยบรรทัดนี้std::moveจะส่งaไปยังค่า rvalue และother_functionจะได้รับวัตถุเป็นวัตถุที่ไม่มีชื่อ แน่นอนถ้าother_functionไม่มีการบรรทุกเกินพิกัดที่เฉพาะเจาะจงในการทำงานกับวัตถุที่ไม่มีชื่อความแตกต่างนี้ไม่สำคัญ

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

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

นั่นคือลายเซ็นของฟังก์ชั่นที่ใช้แม่บทการส่งต่อที่สมบูรณ์แบบนำมาใช้ใน C ++ 11 std::forwardโดยวิธีการของ ฟังก์ชันนี้ใช้ประโยชน์จากกฎของการสร้างอินสแตนซ์ของเทมเพลต:

 `A& && == A&`
 `A&& && == A&&`

ดังนั้นหากTการอ้างอิง lvalue ไปที่A( T = A &) aก็เช่นกัน ( A &&& => A &) หากTมีการอ้างอิง rvalue ไปA, aยัง (A && && => เอ &&) ในทั้งสองกรณีaเป็นวัตถุที่มีชื่อในขอบเขตที่แท้จริง แต่Tมีข้อมูลของ "ประเภทอ้างอิง" ของมันจากมุมมองของขอบเขตผู้โทร ข้อมูลนี้ ( T) จะถูกส่งเป็นพารามิเตอร์แม่แบบforwardและ '' Tจะย้ายหรือไม่เป็นไปตามประเภทของ


20

มันเหมือนกับความหมายของการคัดลอก แต่แทนที่จะต้องทำซ้ำข้อมูลทั้งหมดที่คุณได้รับเพื่อขโมยข้อมูลจากวัตถุที่ถูก "ย้าย" จาก


13

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

ความหมายของการย้ายนั้นเป็นประเภทที่ผู้ใช้กำหนดเองพร้อมตัวสร้างที่ใช้การอ้างอิง r-value (การอ้างอิงชนิดใหม่โดยใช้ && (ใช่แอมป์สองตัว)) ซึ่งไม่ใช่แบบ const นี่เรียกว่าตัวสร้างการย้าย ตัวสร้างการย้ายทำอะไรได้ดีแทนที่จะคัดลอกหน่วยความจำจากอาร์กิวเมนต์แหล่งที่มามัน 'ย้าย' หน่วยความจำจากแหล่งไปยังปลายทาง

คุณต้องการทำเช่นนั้นเมื่อใด well std :: vector เป็นตัวอย่างสมมติว่าคุณสร้าง std ชั่วคราว :: vector และคุณส่งคืนจากฟังก์ชันบอกว่า:

std::vector<foo> get_foos();

คุณจะมีค่าใช้จ่ายจากตัวสร้างการคัดลอกเมื่อฟังก์ชันส่งคืนถ้า (และจะเป็น C ++ 0x) std :: vector มีตัวสร้างการย้ายแทนที่จะคัดลอกมันสามารถตั้งค่าตัวชี้และ 'ย้าย' การจัดสรรแบบไดนามิก หน่วยความจำในอินสแตนซ์ใหม่ มันเหมือนกับซีแมนทิกส์การโอนสิทธิ์การเป็นเจ้าของด้วย std :: auto_ptr


1
ฉันไม่คิดว่านี่เป็นตัวอย่างที่ดีเพราะในตัวอย่างฟังก์ชันคืนค่าเหล่านี้การเพิ่มประสิทธิภาพค่าตอบแทนอาจเป็นการกำจัดการคัดลอกไปแล้ว
Zan Lynx

7

เพื่อแสดงความต้องการย้ายความหมายลองพิจารณาตัวอย่างนี้โดยไม่ต้องมีความหมายย้าย:

นี่คือฟังก์ชั่นที่รับอ็อบเจกต์ประเภทTและส่งคืนออบเจ็กต์ประเภทเดียวกันT:

T f(T o) { return o; }
  //^^^ new object constructed

ฟังก์ชั่นด้านบนใช้การเรียกตามค่าซึ่งหมายความว่าเมื่อฟังก์ชั่นนี้เรียกว่าวัตถุจะต้องสร้างขึ้นเพื่อใช้งานโดยฟังก์ชั่น
เนื่องจากฟังก์ชั่นส่งคืนตามมูลค่าวัตถุใหม่อีกรายการหนึ่งถูกสร้างขึ้นสำหรับค่าส่งคืน:

T b = f(a);
  //^ new object constructed

มีการสร้างวัตถุใหม่สองรายการหนึ่งในนั้นเป็นวัตถุชั่วคราวที่ใช้สำหรับช่วงเวลาของฟังก์ชันเท่านั้น

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


ทีนี้ลองมาดูกันว่าตัวสร้างสำเนาทำอะไร

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

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

ด้วยซีแมนทิกส์ย้ายตอนนี้เป็นไปได้ที่จะทำให้งานนี้ได้ผลเสียน้อยที่สุดเพียงแค่ย้ายข้อมูลแทนที่จะคัดลอก

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

การย้ายข้อมูลเกี่ยวข้องกับการเชื่อมโยงข้อมูลกับวัตถุใหม่ และไม่มีการคัดลอกเกิดขึ้นเลย

นี่คือความสำเร็จที่มีการrvalueอ้างอิง
การrvalueอ้างอิงทำงานคล้ายกับการlvalueอ้างอิงที่มีความแตกต่างที่สำคัญอย่างหนึ่ง:
การอ้างอิง rvalue สามารถเคลื่อนย้ายได้และlvalueไม่สามารถทำได้

จากcppreference.com :

เพื่อให้การรับประกันข้อยกเว้นที่แข็งแกร่งเป็นไปได้ผู้สร้างการย้ายที่ผู้ใช้กำหนดเองไม่ควรโยนข้อยกเว้น ในความเป็นจริงคอนเทนเนอร์มาตรฐานมักใช้ std :: move_if_noexcept เพื่อเลือกระหว่างการย้ายและการคัดลอกเมื่อต้องการย้ายองค์ประกอบของคอนเทนเนอร์ หากทั้งตัวคัดลอกและตัวย้ายถูกจัดเตรียมความละเอียดเกินพิกัดจะเลือกตัวสร้างการย้ายหากอาร์กิวเมนต์เป็น rvalue (เช่น prvalue เช่นชั่วคราวแบบไม่มีชื่อหรือ xvalue เช่นผลลัพธ์ของ std :: move) และเลือกตัวสร้างสำเนาหาก อาร์กิวเมนต์เป็น lvalue (ชื่อวัตถุหรือฟังก์ชั่น / ผู้ประกอบการกลับมาอ้างอิง lvalue) หากมีเพียงตัวสร้างการคัดลอกไว้ให้หมวดหมู่อาร์กิวเมนต์ทั้งหมดเลือก (ตราบเท่าที่ใช้การอ้างอิงถึง const เนื่องจากค่า rvalues ​​สามารถผูกกับการอ้างอิง const) ซึ่งทำให้การคัดลอกทางเลือกสำหรับการย้ายเมื่อการย้ายไม่พร้อมใช้งาน ในหลาย ๆ สถานการณ์ตัวสร้างการเคลื่อนย้ายได้รับการปรับให้เหมาะสมแม้ว่าพวกเขาจะสร้างผลข้างเคียงที่สังเกตได้ดูการคัดลอก คอนสตรัคถูกเรียกว่า 'ย้ายคอนสตรัคเตอร์' เมื่อใช้การอ้างอิง rvalue เป็นพารามิเตอร์ ไม่จำเป็นต้องย้ายสิ่งใด ๆ คลาสไม่จำเป็นต้องมีทรัพยากรที่จะย้ายและ 'ตัวสร้างการย้าย' อาจไม่สามารถย้ายทรัพยากรได้ในกรณีที่อนุญาต (แต่อาจจะไม่สมเหตุสมผล) ที่พารามิเตอร์เป็น const rvalue การอ้างอิง (const T&&)


7

ฉันกำลังเขียนสิ่งนี้เพื่อให้แน่ใจว่าฉันเข้าใจถูกต้อง

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

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

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

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


"A" การมอบหมายย้าย "ช่วยให้โปรแกรมเมอร์สามารถแทนที่พฤติกรรมการคัดลอกเริ่มต้นและแทนการอ้างอิงการอ้างอิงไปยังวัตถุซึ่งหมายความว่าไม่มีการคัดลอกเลยและการดำเนินการสลับนั้นเร็วกว่ามาก" - การอ้างสิทธิ์เหล่านี้ไม่ชัดเจนและทำให้เข้าใจผิด ในการสลับสองวัตถุxและyคุณไม่สามารถเพียง"สลับการอ้างอิงไปยังวัตถุ" ; อาจเป็นไปได้ว่าวัตถุนั้นมีพอยน์เตอร์ที่อ้างอิงข้อมูลอื่นและพอยน์เตอร์เหล่านั้นสามารถสลับได้ แต่ตัวดำเนินการย้ายไม่จำเป็นต้องสลับอะไร พวกเขาอาจล้างข้อมูลจากวัตถุที่ย้ายจากแทนที่จะเก็บรักษาข้อมูลปลายทางในนั้น
Tony Delroy

คุณสามารถเขียนได้swap()โดยไม่ต้องมีความหมายย้าย "การมอบหมายการย้ายสามารถเรียกใช้โดยการเรียกวิธี std :: move ()" - บางครั้งจำเป็นต้องใช้std::move()- แม้ว่ามันจะไม่ได้ย้ายอะไรเลย - เพียงแค่ให้คอมไพเลอร์รู้ว่าอาร์กิวเมนต์นั้นสามารถเคลื่อนย้ายได้บางครั้งstd::forward<>()(พร้อมการอ้างอิงการส่งต่อ) และเวลาอื่น ๆ ที่คอมไพเลอร์รู้ค่าสามารถเคลื่อนย้ายได้
Tony Delroy

-2

นี่คือคำตอบจากหนังสือ "ภาษาการเขียนโปรแกรม C ++" โดย Bjarne Stroustrup หากคุณไม่ต้องการดูวิดีโอคุณสามารถดูข้อความด้านล่าง:

พิจารณาตัวอย่างนี้ การกลับมาจากโอเปอเรเตอร์ + เกี่ยวข้องกับการคัดลอกผลลัพธ์จากตัวแปรโลคัลresและในบางแห่งที่ผู้เรียกสามารถเข้าถึงได้

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

เราไม่ต้องการสำเนาจริงๆ เราแค่อยากได้ผลลัพธ์จากฟังก์ชั่น ดังนั้นเราต้องย้ายเวกเตอร์แทนที่จะคัดลอก เราสามารถกำหนดตัวสร้างการย้ายดังนี้:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&& หมายถึง "การอ้างอิง rvalue" และเป็นการอ้างอิงถึงการที่เราสามารถผูก rvalue ได้ "rvalue" 'มีวัตถุประสงค์เพื่อเสริม "lvalue" ซึ่งหมายถึง "สิ่งที่สามารถปรากฏทางด้านซ้ายมือของงานที่มอบหมาย" ดังนั้นค่า rvalue หมายถึง "ค่าที่คุณไม่สามารถกำหนดให้" อย่างเช่นจำนวนเต็มที่ส่งคืนโดยการเรียกใช้ฟังก์ชันและresตัวแปรโลคัลในตัวดำเนินการ + () สำหรับเวกเตอร์

ตอนนี้คำสั่งreturn res;จะไม่คัดลอก!

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