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


14

การเพิ่มประสิทธิภาพฐานที่ว่างเปล่าดีมาก อย่างไรก็ตามมันมาพร้อมกับข้อ จำกัด ดังต่อไปนี้:

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

เพื่ออธิบายข้อ จำกัด นี้ให้พิจารณารหัสต่อไปนี้ static_assertจะล้มเหลว ในขณะที่การเปลี่ยนแปลงอย่างใดอย่างหนึ่งFooหรือBarเพื่อสืบทอดจากBase2จะเป็นการหลีกเลี่ยงข้อผิดพลาด:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

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

โดยเฉพาะอย่างยิ่งทำไม subobjects ทั้งสองควรจะต้องมีที่อยู่ที่แตกต่างกัน? ในข้างต้นBarเป็นประเภทและfooเป็นตัวแปรสมาชิกประเภทนั้น ฉันไม่เห็นว่าทำไมชั้นฐานของBarเรื่องถึงชั้นฐานของประเภทfooหรือในทางกลับกัน

อันที่จริงฉันมีอะไรถ้าฉันคาดหวังว่า&fooจะเป็นเช่นเดียวกับที่อยู่ของBarอินสแตนซ์ที่มีมัน - มันจะต้องอยู่ในสถานการณ์อื่น(1) (1)หลังจากทั้งหมดฉันไม่ได้ทำอะไรแฟนซีกับการvirtualสืบทอดคลาสพื้นฐานว่างเปล่าโดยไม่คำนึงถึงและการคอมไพล์ด้วยBase2แสดงว่าไม่มีอะไรแตกในกรณีนี้โดยเฉพาะ

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

สมมติว่าคำตอบควรเป็น C ++ 11 หรือใหม่กว่า (ขณะนี้ฉันใช้ C ++ 17)

(1)หมายเหตุ: EBO ได้รับการอัพเกรดใน C ++ 11 และโดยเฉพาะอย่างยิ่งกลายเป็นข้อบังคับสำหรับStandardLayoutTypes (แม้ว่าBarเหนือไม่ใช่ไม่ใช่StandardLayoutType)


4
เหตุผลที่คุณยกมา (" ตั้งแต่ subobjects ฐานสองประเภทเดียวกันจะต้องมีที่อยู่ที่แตกต่างกัน ") สั้นอย่างไร วัตถุที่แตกต่างประเภทเดียวกันจะต้องมีที่อยู่ที่แตกต่างกันและข้อกำหนดนี้ทำให้มั่นใจได้ว่าเราจะไม่ผิดกฎนั้น หากการเพิ่มประสิทธิภาพฐานที่ว่างเปล่านำมาใช้ที่นี่เราจะได้Base *a = new Bar(); Base *b = a->foo;มีa==bแต่aและbเห็นได้ชัดว่าวัตถุที่แตกต่าง (อาจจะมีการแทนที่วิธีการเสมือนแตกต่างกัน)
Toby Speight

1
คำตอบของทนายความภาษาคือการอ้างถึงส่วนที่เกี่ยวข้องของข้อมูลจำเพาะ และดูเหมือนคุณรู้แล้วเกี่ยวกับเรื่องนั้น
Deduplicator

3
ฉันไม่แน่ใจว่าฉันเข้าใจคำตอบแบบไหนที่คุณกำลังมองหาที่นี่ รูปแบบวัตถุ C ++ คืออะไร มีข้อ จำกัด เนื่องจากโมเดลวัตถุต้องการ คุณกำลังมองหาอะไรอีกนอกจากนั้น?
Nicol Bolas

@TobySpeight วัตถุที่แตกต่างประเภทเดียวกันจะต้องมีที่อยู่ที่แตกต่างกันเป็นไปได้อย่างง่ายดายที่จะทำลายกฎนี้ในโปรแกรมที่มีพฤติกรรมที่กำหนดไว้อย่างดี
นักกฎหมายด้านภาษา

@TobySpeight ไม่ฉันไม่ได้หมายความว่าคุณลืมที่จะพูดเกี่ยวกับอายุการใช้งาน: "วัตถุที่แตกต่างกันของชนิดเดียวกันภายในชีวิตของพวกเขา " มีความเป็นไปได้ที่จะมีวัตถุหลายประเภทที่เหมือนกันทั้งหมดมีชีวิตอยู่ในที่อยู่เดียวกัน มีอย่างน้อย 2 ข้อบกพร่องในการใช้ถ้อยคำอนุญาตนี้
ทนายความภาษา

คำตอบ:


4

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

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

แต่ด้วย C ++ 20 จะมีแอตทริบิวต์ใหม่[[no_unique_address]]ที่บอกคอมไพเลอร์ที่เป็นสมาชิกของข้อมูลที่ไม่คงที่อาจไม่จำเป็นต้องมีอยู่เฉพาะ (เทคนิคการพูดมันจะอาจทับซ้อนกัน [intro.object] / 7 )

นี่ก็หมายความว่า (เน้นที่เหมือง)

สมาชิกข้อมูลที่ไม่คงที่สามารถแบ่งปันที่อยู่ของสมาชิกข้อมูลไม่คงที่อีกหรือที่ของชั้นฐาน [... ]

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

ตัวอย่างของปัญหาผิดพลาดผ่านทางนี้

เนื่องจากดูเหมือนว่าคลาสที่ว่างเปล่าอาจไม่มีวิธีการเสมือนให้ฉันเพิ่มตัวอย่างที่สาม:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

แต่การโทรสองครั้งสุดท้ายเหมือนกัน

ตัวอย่างเก่า (อาจไม่ตอบคำถามเนื่องจากคลาสที่ว่างเปล่าอาจไม่มีวิธีเสมือนดูเหมือน)

พิจารณาในรหัสของคุณด้านบน (พร้อมเพิ่ม destructors เสมือน) ตัวอย่างต่อไปนี้

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

แต่คอมไพเลอร์ควรแยกความแตกต่างระหว่างสองกรณีนี้อย่างไร

และอาจมีการประดิษฐ์น้อยกว่าเล็กน้อย:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

แต่สองอันสุดท้ายนั้นเหมือนกันถ้าเรามีการปรับคลาสฐานให้ว่าง!


1
หนึ่งอาจโต้แย้งว่าสายที่สองมีพฤติกรรมที่ไม่ได้กำหนดอย่างไรก็ตาม ดังนั้นคอมไพเลอร์ไม่จำเป็นต้องแยกแยะอะไรเลย
StoryTeller - Unslander Monica

1
คลาสที่มีสมาชิกเสมือนใด ๆ ไม่ว่างเปล่าดังนั้นจึงไม่เกี่ยวข้องที่นี่!
Deduplicator

1
@Dupuplicator คุณมีคำพูดมาตรฐานในเรื่องนั้นหรือไม่? Cppref บอกเราว่าคลาสที่ว่างเปล่าคือ "คลาสหรือโครงสร้างที่ไม่มีสมาชิกข้อมูลที่ไม่คงที่"
n314159

1
@ n314159 std::is_emptyบน cppreference นั้นซับซ้อนกว่ามาก เดียวกันจากร่างปัจจุบันใน eel.is
Deduplicator

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