เมื่อใดที่จะสร้างประเภทที่ไม่สามารถเคลื่อนย้ายได้ใน C ++ 11


127

ฉันแปลกใจที่สิ่งนี้ไม่ปรากฏในผลการค้นหาของฉันฉันคิดว่ามีคนถามเรื่องนี้มาก่อนเนื่องจากประโยชน์ของความหมายการย้ายใน C ++ 11:

เมื่อใดที่ฉันต้อง (หรือเป็นความคิดที่ดีสำหรับฉัน) ทำให้คลาสไม่สามารถเคลื่อนย้ายได้ใน C ++ 11

(เหตุผลอื่นนอกเหนือจากปัญหาความเข้ากันได้กับรหัสที่มีอยู่นั่นคือ)


2
การเพิ่มกำลังนำหน้าไปหนึ่งก้าวเสมอ - "แพงในการย้ายประเภท" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

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

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

1
@ Mehrdad: ฉันแค่บอกว่า "T มีตัวสร้างการย้าย" และ " T x = std::move(anotherT);ถูกกฎหมาย" นั้นไม่เทียบเท่า หลังเป็นคำขอย้ายซึ่งอาจถอยกลับไปที่สำเนา ctor ในกรณีที่ T ไม่มี ctor การย้าย แล้ว "เคลื่อนย้ายได้" หมายความว่าอย่างไรกันแน่?
sellibitze

1
@Mehrdad: ดูส่วนไลบรารีมาตรฐาน C ++ ว่า "MoveConstructible" หมายถึงอะไร ตัวทำซ้ำบางตัวอาจไม่มีตัวสร้างการย้าย แต่ยังคงเป็น MoveConstructible ระวังคำจำกัดความที่แตกต่างกันของผู้คน "เคลื่อนย้ายได้" ในใจ
sellibitze

คำตอบ:


110

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

ประเภท mutex ดั้งเดิมของ OS (เช่นpthread_mutex_tบนแพลตฟอร์ม POSIX) อาจไม่ใช่ "ตำแหน่งไม่แปรผัน" ซึ่งหมายความว่าที่อยู่ของวัตถุเป็นส่วนหนึ่งของค่าของมัน ตัวอย่างเช่นระบบปฏิบัติการอาจเก็บรายการตัวชี้ไปยังวัตถุ mutex ที่เริ่มต้นทั้งหมด หากstd::mutexมีประเภท mutex ดั้งเดิมของ OS เป็นสมาชิกข้อมูลและที่อยู่ของประเภทเนทีฟต้องคงที่ (เนื่องจากระบบปฏิบัติการเก็บรักษารายการตัวชี้ไปยัง mutexes ของมัน) ก็std::mutexจะต้องจัดเก็บประเภท mutex ดั้งเดิมบนฮีปดังนั้นจึงจะอยู่ที่ ตำแหน่งเดียวกันเมื่อย้ายระหว่างstd::mutexวัตถุหรือstd::mutexต้องไม่เคลื่อนย้าย การจัดเก็บไว้ในฮีปเป็นไปไม่ได้เนื่องจาก a std::mutexมีตัวconstexprสร้างและต้องมีสิทธิ์สำหรับการเริ่มต้นคงที่ (เช่นการเริ่มต้นแบบคงที่) เพื่อให้โกลบอลstd::mutexรับประกันว่าจะสร้างขึ้นก่อนที่โปรแกรมจะเริ่มทำงานดังนั้นตัวสร้างจึงไม่สามารถใช้งานnewได้ ดังนั้นทางเลือกเดียวที่เหลือคือstd::mutexการเคลื่อนย้ายไม่ได้

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

มีข้อโต้แย้งอีกประการหนึ่งสำหรับการไม่เคลื่อนย้ายstd::mutexซึ่งเป็นเรื่องยากมากที่จะทำอย่างปลอดภัยเพราะคุณจำเป็นต้องรู้ว่าไม่มีใครพยายามล็อค mutex ในขณะที่มีการเคลื่อนย้าย เนื่องจาก mutexes เป็นหนึ่งในหน่วยการสร้างที่คุณสามารถใช้เพื่อป้องกันการแข่งขันข้อมูลได้จึงจะโชคร้ายหากพวกเขาไม่ปลอดภัยต่อการแข่งขัน! ด้วยการเคลื่อนย้ายไม่ได้std::mutexคุณจะรู้ว่าสิ่งเดียวที่ทุกคนสามารถทำได้เมื่อมันถูกสร้างขึ้นและก่อนที่มันจะถูกทำลายคือการล็อกและปลดล็อกและการดำเนินการเหล่านั้นได้รับการรับรองอย่างชัดเจนว่าเธรดปลอดภัยและไม่แนะนำการแข่งขันข้อมูล อาร์กิวเมนต์เดียวกันนี้ใช้กับstd::atomic<T>อ็อบเจ็กต์: เว้นเสียแต่ว่าพวกมันจะถูกเคลื่อนย้ายโดยอะตอมมันเป็นไปไม่ได้ที่จะย้ายพวกมันอย่างปลอดภัยเธรดอื่นอาจพยายามเรียกcompare_exchange_strongบนวัตถุทันทีที่มีการเคลื่อนย้าย ดังนั้นอีกกรณีหนึ่งที่ไม่ควรเคลื่อนย้ายประเภทคือที่ซึ่งเป็นหน่วยการสร้างระดับต่ำของรหัสพร้อมกันที่ปลอดภัยและต้องตรวจสอบความเป็นอะตอมของการดำเนินการทั้งหมด หากค่าวัตถุอาจถูกย้ายไปยังวัตถุใหม่เมื่อใดก็ได้คุณจะต้องใช้ตัวแปรอะตอมเพื่อปกป้องตัวแปรอะตอมทุกตัวเพื่อให้คุณรู้ว่ามันปลอดภัยที่จะใช้หรือถูกย้าย ... และตัวแปรอะตอมเพื่อป้องกัน ตัวแปรอะตอมนั้นและอื่น ๆ ...

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


17
+1 ตัวอย่างที่แปลกใหม่น้อยกว่าของสิ่งที่ไม่สามารถย้ายได้เนื่องจากมีที่อยู่พิเศษคือโหนดในโครงสร้างกราฟที่กำหนดทิศทาง
Potatoswatter

3
ถ้า mutex ไม่สามารถคัดลอกและเคลื่อนย้ายไม่ได้ฉันจะคัดลอกหรือย้ายวัตถุที่มี mutex ได้อย่างไร? (เช่นเดียวกับคลาสที่ปลอดภัยของเธรดที่มี mutex ของตัวเองสำหรับการซิงโครไนซ์ ... )
tr3w

4
@ tr3w คุณทำไม่ได้เว้นแต่คุณจะสร้าง mutex บนฮีปและถือไว้ผ่าน unique_ptr หรือคล้ายกัน
Jonathan Wakely

2
@ tr3w: คุณจะไม่ย้ายทั้งคลาสยกเว้นส่วน mutex หรือ?
user541686

3
@BenVoigt แต่วัตถุใหม่จะมี mutex ของตัวเอง ฉันคิดว่าเขาหมายถึงการดำเนินการย้ายที่กำหนดโดยผู้ใช้ซึ่งจะย้ายสมาชิกทั้งหมดยกเว้นสมาชิก mutex แล้วถ้าวัตถุเก่าหมดอายุล่ะ? mutex หมดอายุด้วย
Jonathan Wakely

57

คำตอบสั้น ๆ : หากประเภทสามารถคัดลอกได้ก็ควรเคลื่อนย้ายได้เช่นกัน อย่างไรก็ตามการย้อนกลับไม่เป็นความจริง: บางประเภทstd::unique_ptrสามารถเคลื่อนย้ายได้ แต่ก็ไม่สมเหตุสมผลที่จะคัดลอก ซึ่งเป็นประเภทที่เคลื่อนไหวอย่างเดียวตามธรรมชาติ

คำตอบยาวกว่านี้เล็กน้อย ...

มีสองประเภทใหญ่ ๆ (ในกลุ่มวัตถุประสงค์พิเศษอื่น ๆ เช่นลักษณะ):

  1. ราคาเหมือนประเภทเช่นหรือint vector<widget>สิ่งเหล่านี้แสดงถึงคุณค่าและควรคัดลอกได้ตามธรรมชาติ ใน C ++ 11 โดยทั่วไปคุณควรคิดว่าการย้ายเป็นการเพิ่มประสิทธิภาพของสำเนาดังนั้นประเภทที่สามารถคัดลอกได้ทั้งหมดควรเคลื่อนย้ายได้ตามธรรมชาติ ... การเคลื่อนย้ายเป็นเพียงวิธีที่มีประสิทธิภาพในการทำสำเนาในกรณีที่คุณไม่ได้ทำ ไม่ต้องการวัตถุดั้งเดิมอีกต่อไปและกำลังจะทำลายมันต่อไป

  2. ชนิดที่เหมือนการอ้างอิงที่มีอยู่ในลำดับชั้นการสืบทอดเช่นคลาสพื้นฐานและคลาสที่มีฟังก์ชันสมาชิกเสมือนหรือได้รับการป้องกัน โดยปกติแล้วสิ่งเหล่านี้จะถือโดยตัวชี้หรือการอ้างอิงซึ่งมักจะเป็นbase*หรือbase&และดังนั้นอย่าให้มีโครงสร้างสำเนาเพื่อหลีกเลี่ยงการแบ่งส่วน หากคุณต้องการรับวัตถุอื่นเช่นเดียวกับที่มีอยู่คุณมักจะเรียกใช้ฟังก์ชันเสมือนเช่นclone. สิ่งเหล่านี้ไม่จำเป็นต้องย้ายโครงสร้างหรือการมอบหมายด้วยเหตุผลสองประการ: ไม่สามารถคัดลอกได้และมีการดำเนินการ "ย้าย" ตามธรรมชาติที่มีประสิทธิภาพมากขึ้นแล้วคุณเพียงแค่คัดลอก / ย้ายตัวชี้ไปยังวัตถุและตัววัตถุเองก็ไม่มี ต้องย้ายไปยังตำแหน่งหน่วยความจำใหม่เลย

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


61
Herb Sutterตัวจริงจะลุกขึ้นยืนได้ไหม? :)
fredoverflow

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

7
ฉันคิดว่าstd::mutexมันไม่สามารถเคลื่อนย้ายได้เนื่องจาก POSIX mutexes ถูกใช้ตามที่อยู่
ลูกสุนัข

9
@SChepurin: ที่จริงเรียกว่า HerbOverflow
sbi

26
กำลังได้รับคะแนนโหวตจำนวนมากไม่มีใครสังเกตเห็นว่าเมื่อใดควรย้ายประเภทเท่านั้นซึ่งไม่ใช่คำถาม? :)
Jonathan Wakely

18

จริงๆแล้วเมื่อฉันค้นหารอบ ๆ ฉันพบว่ามีบางประเภทใน C ++ 11 ไม่สามารถเคลื่อนย้ายได้:

  • ทุกmutexประเภท ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • ทุกatomicประเภท
  • once_flag

เห็นได้ชัดว่ามีการสนทนาเกี่ยวกับเสียงดัง: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... ตัวทำซ้ำไม่ควรเคลื่อนย้าย?! อะไรทำไม?
user541686

ใช่ฉันคิดว่าiterators / iterator adaptorsควรแก้ไขเพราะ C ++ 11 มี move_iterator?
billz

โอเคตอนนี้ฉันแค่สับสน คุณกำลังพูดถึงตัวทำซ้ำที่ย้ายเป้าหมายหรือเกี่ยวกับการย้ายตัวทำซ้ำด้วยตัวเอง ?
541686

1
std::reference_wrapperดังนั้น ตกลงคนอื่นดูเหมือนจะไม่สามารถเคลื่อนย้ายได้
Christian Rau

1
เหล่านี้ดูเหมือนจะตกอยู่ในสามประเภท: 1. ระดับต่ำประเภทเห็นพ้องที่เกี่ยวข้อง (อะตอม, mutexes) 2. polymorphic คลาสฐาน ( ios_base, type_info, facet) 3. สิ่งที่แปลกสารพัน ( sentry) อาจเป็นคลาสเดียวที่ไม่สามารถเคลื่อนย้ายได้ที่โปรแกรมเมอร์โดยเฉลี่ยจะเขียนอยู่ในประเภทที่สอง
Philipp

0

อีกเหตุผลหนึ่งที่ฉันพบ - ประสิทธิภาพ สมมติว่าคุณมีคลาส 'a' ซึ่งมีค่า คุณต้องการส่งออกอินเทอร์เฟซที่อนุญาตให้ผู้ใช้เปลี่ยนค่าในช่วงเวลา จำกัด (สำหรับขอบเขต)

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

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

ถ้าฉันทำให้ change_value_guard สามารถเคลื่อนย้ายได้ฉันจะต้องเพิ่ม 'if' ให้กับตัวทำลายซึ่งจะตรวจสอบว่าตัวป้องกันถูกย้ายจากหรือไม่นั่นเป็นสิ่งที่เพิ่มขึ้นหากและผลกระทบต่อประสิทธิภาพ

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

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