อายุการใช้งานของวัตถุกับความหมายของการย้าย


13

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

ตั้งแต่ C ++ 11 ตอนนี้เรามีความหมายย้าย สำหรับคลาสที่รองรับการย้ายการย้ายจากวัตถุไม่สิ้นสุดอย่างเป็นทางการตลอดอายุการย้ายควรจะปล่อยให้มันอยู่ในสถานะ "ถูกต้อง" บางอย่าง

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

หากต้องการทำให้เป็นรูปธรรมสมมติว่าฉันมีประเภททรัพยากรที่ไม่สามารถคัดลอกได้ แต่เคลื่อนย้ายได้เช่น:

class opaque {
  opaque(const opaque &) = delete;

public:
  opaque(opaque &&);

  ...

  void mysterious();
  void mysterious(int);
  void mysterious(std::vector<std::string>);
};

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

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { o_->mysterious(); }
  void operator()(int i) { o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};

ในcopyable_opaqueวัตถุนี้ค่าคงที่ของคลาสที่สร้างขึ้นในการก่อสร้างคือสมาชิกo_จะชี้ไปยังวัตถุที่ถูกต้องเสมอเนื่องจากไม่มี ctor เริ่มต้นและ ctor เดียวที่ไม่ใช่ ctor คัดลอกรับประกันเหล่านี้ ทั้งหมดของoperator()วิธีการคิดว่าคงนี้ถือและรักษามันไว้หลังจากนั้น

อย่างไรก็ตามหากวัตถุถูกย้ายจากนั้นo_จะชี้ไปที่ไม่มีอะไร และหลังจากนั้นการเรียกเมธอดใด ๆoperator()จะทำให้ UB / a เกิดขัดข้อง

หากวัตถุนั้นไม่เคยถูกเคลื่อนย้ายจากนั้นค่าคงที่จะถูกเก็บรักษาไว้จนถึงการเรียก dtor

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

ความคิด:

  1. ปกติแล้วมันจะเป็นรูปแบบที่ไม่ดีใน C ++ เพื่อสร้างวัตถุซอมบี้ที่จะระเบิดหากคุณแตะมัน
    หากคุณไม่สามารถสร้างวัตถุบางอย่างไม่สามารถสร้างค่าคงที่แล้วโยนข้อยกเว้นจาก ctor หากคุณไม่สามารถรักษาค่าคงที่ในวิธีการบางอย่างให้ส่งสัญญาณข้อผิดพลาดอย่างใดอย่างหนึ่งและย้อนกลับ สิ่งนี้ควรแตกต่างจากการย้ายจากวัตถุหรือไม่?

  2. มันเพียงพอหรือไม่ที่จะจัดทำเอกสาร "หลังจากวัตถุนี้ถูกย้ายไปมันผิดกฎหมาย (UB) ที่จะทำอะไรกับมันนอกจากทำลายมัน" ในส่วนหัว?

  3. เป็นการดีกว่าที่จะยืนยันอย่างต่อเนื่องว่าใช้ได้ในการเรียกใช้แต่ละวิธีหรือไม่

ชอบมาก

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { assert(o_); o_->mysterious(); }
  void operator()(int i) { assert(o_); o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};

การยืนยันไม่ได้ปรับปรุงพฤติกรรมอย่างมีนัยสำคัญและจะทำให้ช้าลง หากโครงการของคุณใช้รูปแบบ "build build / debug build" แทนที่จะใช้กับการยืนยันเสมอฉันคิดว่านี่น่าสนใจกว่าเนื่องจากคุณไม่ต้องจ่ายเงินสำหรับเช็คใน build build ถ้าคุณไม่มี debug builds จริง ๆ แล้วมันก็ไม่น่าสนใจ

  1. เป็นการดีกว่าหรือที่จะทำให้คลาสคัดลอกได้ แต่ไม่สามารถย้ายได้
    สิ่งนี้ดูเหมือนว่าจะไม่ดีและทำให้เกิดผลการทำงาน แต่ก็แก้ไขปัญหา "ไม่แปรเปลี่ยน" ได้อย่างตรงไปตรงมา

คุณคิดว่าอะไรคือ "แนวทางปฏิบัติที่ดีที่สุด" ที่เกี่ยวข้องที่นี่



คำตอบ:


20

ปกติแล้วมันจะเป็นรูปแบบที่ไม่ดีใน C ++ เพื่อสร้างวัตถุซอมบี้ที่จะระเบิดหากคุณแตะมัน

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

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

void func(std::vector<int> &v)
{
  v[0] = 5;
}

ฟังก์ชั่นนี้ปลอดภัยหรือไม่ ไม่มี ผู้ใช้สามารถผ่านช่องว่าง vectorได้ ดังนั้นฟังก์ชั่นนี้มีเงื่อนไขล่วงหน้าที่vมีองค์ประกอบอย่างน้อยหนึ่งองค์ประกอบ ถ้ามันไม่ได้แล้วคุณจะได้รับ UB funcเมื่อคุณเรียก

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

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

ไลบรารีมาตรฐาน C ++ กำหนดกฎ "valid-but-unspecified" สิ่งนี้บอกว่ายกเว้นว่ามาตรฐานบอกเป็นอย่างอื่นวัตถุทุกชิ้นที่ถูกย้ายจากจะถูกต้อง (เป็นวัตถุทางกฎหมายประเภทนั้น) แต่สถานะเฉพาะของวัตถุนั้นไม่ได้ระบุไว้ วิธีการหลายองค์ประกอบไม่ย้ายจากvectorมี? มันไม่ได้พูด

ซึ่งหมายความว่าคุณจะไม่สามารถเรียกใช้ฟังก์ชันใด ๆ ซึ่งมีใด ๆที่จำเป็น vector::operator[]มีเงื่อนไขที่ว่าvectorมีองค์ประกอบอย่างน้อยหนึ่งรายการ เนื่องจากคุณไม่ทราบสถานะของvectorคุณจึงไม่สามารถโทรได้ มันจะไม่ดีไปกว่าการโทรfuncโดยไม่ได้ตรวจสอบก่อนว่าสิ่งที่vectorไม่ว่างเปล่า

แต่นี่ก็หมายความว่าฟังก์ชั่นที่ไม่มีเงื่อนไขเป็นสิ่งที่ดี นี่คือรหัส C ++ 11 ที่ถูกกฎหมาย:

vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2{std::move(v1)};
v1.assign({6, 7, 8, 9, 10});

vector::assignไม่มีเงื่อนไขก่อน มันจะทำงานกับvectorวัตถุที่ใช้ได้แม้กระทั่งวัตถุที่ถูกย้ายไปแล้ว

ดังนั้นคุณไม่ได้สร้างวัตถุที่เสีย คุณกำลังสร้างวัตถุที่ไม่ทราบสถานะ

หากคุณไม่สามารถสร้างวัตถุบางอย่างไม่สามารถสร้างค่าคงที่แล้วโยนข้อยกเว้นจาก ctor หากคุณไม่สามารถรักษาค่าคงที่ในวิธีการบางอย่างให้ส่งสัญญาณข้อผิดพลาดอย่างใดอย่างหนึ่งและย้อนกลับ สิ่งนี้ควรแตกต่างจากการย้ายจากวัตถุหรือไม่?

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

น่าเศร้าที่เราไม่สามารถบังคับใช้นี้ด้วยเหตุผลต่างๆ เราต้องยอมรับว่าการขว้างปาเป็นความเป็นไปได้

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

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

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

vector<int> func()
{
  vector<int> v;
  //fill up `v`.
  return v;
}

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

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

มันเพียงพอหรือไม่ที่จะจัดทำเอกสาร "หลังจากวัตถุนี้ถูกย้ายไปมันผิดกฎหมาย (UB) ที่จะทำอะไรกับมันนอกจากทำลายมัน" ในส่วนหัว?

เป็นการดีกว่าที่จะยืนยันอย่างต่อเนื่องว่าใช้ได้ในการเรียกใช้แต่ละวิธีหรือไม่

จุดรวมของการมีเงื่อนไขเบื้องต้นคือการไม่ตรวจสอบสิ่งต่าง ๆ operator[]มีเงื่อนไขเบื้องต้นที่vectorมีองค์ประกอบพร้อมกับดัชนีที่กำหนด คุณจะได้รับ UB vectorถ้าคุณพยายามที่จะเข้าถึงนอกขนาดของ vector::at ไม่มีเงื่อนไขเช่นนั้น มันจะโยนข้อยกเว้นอย่างชัดเจนหากvectorไม่มีค่าดังกล่าว

มีเงื่อนไขเบื้องต้นสำหรับเหตุผลด้านประสิทธิภาพ พวกเขากำลังเพื่อให้คุณไม่ต้องตรวจสอบสิ่งที่ผู้โทรสามารถตรวจสอบได้ด้วยตนเอง ทุกการโทรไปv[0]ไม่จำเป็นต้องตรวจสอบว่าvว่างเปล่าหรือไม่ เฉพาะคนแรกเท่านั้น

เป็นการดีกว่าหรือที่จะทำให้คลาสคัดลอกได้ แต่ไม่สามารถเคลื่อนย้ายได้?

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

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


5
ดี +1 ฉันจะทราบว่า: "จุดสำคัญของการมีเงื่อนไขคือการไม่ตรวจสอบสิ่งต่าง ๆ " - ฉันไม่คิดว่านี่เป็นการยืนยัน การยืนยันเป็น IMHO เป็นเครื่องมือที่ดีและถูกต้องในการตรวจสอบสิ่งที่จำเป็นต้องมี (ส่วนใหญ่เป็นเวลาอย่างน้อย)
Martin Ba

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