แรงจูงใจและการใช้ตัวสร้างการย้ายใน C ++


17

เมื่อเร็ว ๆ นี้ฉันได้อ่านเกี่ยวกับตัวสร้างการย้ายใน C ++ (ดูตัวอย่างที่นี่ ) และฉันพยายามที่จะเข้าใจวิธีการทำงานและเวลาที่ฉันควรใช้พวกเขา

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

ปกติฉันจะจัดการกับสถานการณ์เช่นนี้

  • โดยการส่งวัตถุโดยการอ้างอิงหรือ
  • โดยการใช้ตัวชี้อัจฉริยะ (เช่น boost :: shared_ptr) เพื่อส่งผ่านวัตถุ (ตัวชี้อัจฉริยะจะถูกคัดลอกแทนวัตถุ)

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


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

คำตอบ:


16

ความหมายของการย้ายข้อมูลนำเสนอมิติทั้งหมดไปยัง C ++ ไม่ใช่เพียงแค่ให้คุณส่งคืนค่าอย่างถูก

ตัวอย่างเช่นหากไม่มีการย้ายความหมายstd::unique_ptrจะไม่ทำงานดูที่std::auto_ptrเลิกใช้งานการย้ายความหมายและนำออกใน C ++ 17 การย้ายทรัพยากรแตกต่างอย่างมากจากการคัดลอก อนุญาตให้ถ่ายโอนความเป็นเจ้าของรายการพิเศษ

ตัวอย่างเช่นอย่ามองstd::unique_ptrเพราะมันถูกกล่าวถึงค่อนข้างดี มาดูกันว่า Vertex Buffer Object ใน OpenGL จุดยอดบัฟเฟอร์แสดงถึงหน่วยความจำบน GPU - มันจำเป็นต้องได้รับการจัดสรรและยกเลิกการจัดสรรโดยใช้ฟังก์ชั่นพิเศษอาจมีข้อ จำกัด แน่นเกี่ยวกับระยะเวลาที่มันสามารถอยู่ได้ นอกจากนี้ยังเป็นสิ่งสำคัญที่มีเพียงเจ้าของคนเดียวเท่านั้นที่ใช้มัน

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

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

เห็นได้ชัดว่าฉันยังไม่ได้ใช้ตัวสร้างการย้าย แต่คุณได้รับความคิด

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


ขอบคุณสำหรับคำตอบ. จะเกิดอะไรขึ้นหากมีคนใช้ตัวชี้ที่ใช้ร่วมกันที่นี่
Giorgio

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

3
@Giorgio คุณสามารถใช้ตัวชี้ที่ใช้ร่วมกันได้ แต่มันอาจผิดไปทางความหมาย ไม่สามารถแชร์บัฟเฟอร์ได้ นอกจากนี้นั่นจะทำให้คุณผ่านตัวชี้ไปยังตัวชี้ (เนื่องจาก vbo นั้นเป็นตัวชี้เฉพาะไปยังหน่วยความจำ GPU) มีคนดูรหัสของคุณในภายหลังอาจสงสัยว่า 'เพราะเหตุใดจึงมีตัวชี้ที่ใช้ร่วมกันที่นี่ มันเป็นทรัพยากรที่ใช้ร่วมกัน? นั่นอาจเป็นข้อผิดพลาด! ' มันจะดีกว่าถ้าจะให้ชัดเจนที่สุดเท่าที่จะทำได้
Max

@Giorgio ใช่นั่นเป็นส่วนหนึ่งของข้อกำหนดด้วย เมื่อ 'renderer' ในกรณีนี้ต้องการจัดสรรคืนทรัพยากรบางส่วน (อาจมีหน่วยความจำไม่เพียงพอสำหรับวัตถุใหม่บน GPU) จะต้องไม่มีการจัดการอื่น ๆ กับหน่วยความจำ การใช้ shared_ptr ที่ผ่านขอบเขตจะใช้งานได้หากคุณไม่เก็บไว้ที่อื่น แต่ทำไมไม่ทำให้ชัดเจนเมื่อคุณสามารถทำได้
Max

@Giorgio ดูการแก้ไขของฉันสำหรับลองอีกครั้งที่ชัดเจน
Max

5

ความหมายของการย้ายไม่จำเป็นต้องเป็นการปรับปรุงที่สำคัญเมื่อคุณคืนค่า - และเมื่อ / ถ้าคุณใช้shared_ptr(หรือสิ่งที่คล้ายกัน) คุณอาจจะมองโลกในแง่ร้ายก่อนเวลาอันควร ในความเป็นจริงคอมไพเลอร์สมัยใหม่ที่มีเหตุผลเกือบทั้งหมดทำในสิ่งที่เรียกว่า Return Value Optimization (RVO) และ Named Return Value Optimization (NRVO) ซึ่งหมายความว่าเมื่อคุณส่งคืนค่าแทนที่จะคัดลอกค่าเลยพวกเขาเพียงแค่ผ่านตัวชี้ที่ซ่อนอยู่ / อ้างอิงถึงตำแหน่งที่ค่าจะได้รับมอบหมายหลังจากกลับมาและฟังก์ชั่นการใช้งานนั้นเพื่อสร้างมูลค่าที่มันจะจบลง มาตรฐาน C ++ มีข้อกำหนดพิเศษเพื่ออนุญาตสิ่งนี้ดังนั้น (ตัวอย่างเช่น) ตัวสร้างสำเนาของคุณมีผลข้างเคียงที่มองเห็นได้ไม่จำเป็นต้องใช้ตัวสร้างสำเนาเพื่อส่งคืนค่า ตัวอย่างเช่น:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

แนวคิดพื้นฐานที่นี่ค่อนข้างง่าย: สร้างคลาสที่มีเนื้อหาเพียงพอที่เราจะหลีกเลี่ยงการคัดลอกถ้าเป็นไปได้ ( std::vectorเราเติมด้วย int แบบสุ่ม 32767) เรามี ctor คัดลอกที่ชัดเจนที่จะแสดงให้เราเห็นเมื่อ / ถ้ามันถูกคัดลอก นอกจากนี้เรายังมีโค้ดอีกเล็กน้อยที่จะทำบางสิ่งด้วยค่าสุ่มในวัตถุดังนั้นเครื่องมือเพิ่มประสิทธิภาพจะไม่กำจัดทุกอย่างเกี่ยวกับคลาสเพียงเพราะมันไม่ทำอะไรเลย

จากนั้นเรามีรหัสบางอย่างเพื่อส่งคืนหนึ่งในวัตถุเหล่านี้จากฟังก์ชั่นและจากนั้นใช้การรวมเพื่อให้แน่ใจว่าวัตถุถูกสร้างขึ้นจริง ๆ ไม่เพียงละเว้นอย่างสมบูรณ์ เมื่อเราเรียกใช้อย่างน้อยที่สุดกับคอมไพเลอร์ตัวล่าสุด / สมัยใหม่เราพบว่าตัวสร้างการคัดลอกที่เราเขียนไม่เคยทำงานเลย - และใช่ฉันค่อนข้างแน่ใจว่าแม้การคัดลอกที่รวดเร็วด้วย a shared_ptrยังช้ากว่าการไม่คัดลอก เลย

การย้ายช่วยให้คุณสามารถทำสิ่งต่าง ๆ ที่คุณไม่สามารถทำได้โดยตรง พิจารณาส่วน "ผสาน" ของการจัดเรียงผสานภายนอก - คุณมี 8 ไฟล์ที่คุณจะรวมเข้าด้วยกัน เป็นการดีที่คุณต้องการที่จะนำทั้ง 8 ของไฟล์เหล่านั้นเป็นvector- แต่เนื่องจากvector( ณ C ++ 03) จะต้องสามารถที่จะคัดลอกองค์ประกอบและifstreamไม่สามารถคัดลอกคุณติดอยู่กับบางส่วนunique_ptr/ shared_ptr, หรือบางอย่างในการสั่งซื้อเพื่อให้สามารถใส่พวกเขาในเวกเตอร์ โปรดทราบว่าแม้ว่าเราจะreserveเว้นวรรค(เช่น) vectorดังนั้นเราจึงมั่นใจได้ว่าifstreamจะไม่มีการคัดลอกจริงๆคอมไพเลอร์จะไม่ทราบดังนั้นรหัสจะไม่คอมไพล์แม้ว่าเราจะรู้ว่าตัวสร้างการคัดลอกจะไม่เป็นเช่นนั้น ใช้อยู่ดี

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

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

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


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

@Giorgio: ใช่ว่าถูกต้องแล้ว ภาษาไม่ได้เพิ่มความหมายของการย้าย มันเพิ่มการอ้างอิง rvalue การอ้างอิง rvalue (ชัดเจนเพียงพอ) สามารถผูกเข้ากับ rvalue ซึ่งในกรณีนี้คุณรู้ว่ามันปลอดภัยที่จะ "ขโมย" การแสดงข้อมูลภายในและเพียงแค่คัดลอกพอยน์เตอร์แทนที่จะทำสำเนาลึก
Jerry Coffin

4

พิจารณา:

vector<string> v;

เมื่อเพิ่มสตริงลงใน v มันจะขยายตามต้องการและในการจัดสรรใหม่แต่ละครั้งสตริงจะต้องถูกคัดลอก ด้วย constructors ย้ายนี่เป็นพื้นไม่ใช่ปัญหา

แน่นอนคุณสามารถทำสิ่งที่ชอบ:

vector<unique_ptr<string>> v;

แต่นั่นจะทำงานได้ดีเพราะstd::unique_ptrใช้ตัวสร้างการเคลื่อนไหวเท่านั้น

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


แต่ถ้าหากstringเรามีตัวอย่างของFooตำแหน่งที่มีสมาชิก 30 ข้อมูล? unique_ptrรุ่นจะไม่ได้มีประสิทธิภาพมากขึ้น?
Vassilis

2

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


ปกติแล้วฉันยังใช้ตัวชี้สมาร์ทเพื่อห่อวัตถุที่ส่งคืนจากฟังก์ชัน / วิธี
Giorgio

1
@Giorgio: แน่นอนทั้งทำให้งงงวยและช้า
DeadMG

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