อ่าน 6 นาที
มรดกเสมือนจริง
การสืบทอดเสมือน (Virtual inheritance ) เป็น เทคนิคใน ภาษา C++ที่ช่วยให้มั่นใจได้ว่า คลาสที่สืบทอดมาจาก คลาสพื้นฐาน จะ ได้รับสำเนา ของ ตัวแปรสมาชิกเพียง ชุดเดียวเท่านั้น...
มรดกเสมือนจริง

การสืบทอดเสมือน (Virtual inheritance ) เป็น เทคนิคใน ภาษา C++ที่ช่วยให้มั่นใจได้ว่า คลาสที่สืบทอดมาจาก คลาสพื้นฐาน จะ ได้รับสำเนา ของ ตัวแปรสมาชิกเพียง ชุดเดียวเท่านั้น หากไม่มีการสืบทอดเสมือน หากคลาสสองคลาสสืบทอดมาจากคลาสพื้นฐานและคลาสพื้นฐานสืบทอดมาจากทั้งคลาสพื้นฐานและ คลาสพื้นฐาน คลาสพื้นฐานจะมีสำเนาของตัวแปรสมาชิกสองชุด คือชุดหนึ่งผ่านทางและอีกชุดหนึ่งผ่านทาง ตัวแปรทั้ง สองชุดนี้สามารถเข้าถึงได้โดยอิสระโดยใช้การกำหนดขอบเขต (scope resolution ) BCADBCDABC
แต่ถ้าคลาสต่างๆBสืบทอดCแบบเสมือนจากคลาสAแล้วอ็อบเจ็กต์ของคลาสDจะมีเพียงชุดตัวแปรสมาชิกชุดเดียวจากคลาสAเท่านั้น
คุณสมบัตินี้มีประโยชน์มากที่สุดสำหรับการสืบทอดแบบหลายทางเนื่องจากทำให้ฐานเสมือนเป็นซับออบเจกต์ ทั่วไป สำหรับคลาสที่สืบทอดและคลาสทั้งหมดที่สืบทอดมาจากคลาสนั้น สามารถใช้เพื่อหลีกเลี่ยงปัญหาเพชรโดยการชี้แจงความกำกวมเกี่ยวกับคลาสบรรพบุรุษที่จะใช้ เนื่องจากจากมุมมองของคลาสที่สืบทอด ( Dในตัวอย่างข้างต้น) ฐานเสมือน ( A) ทำหน้าที่ราวกับว่าเป็นคลาสฐานโดยตรงของDไม่ใช่คลาสที่สืบทอดทางอ้อมผ่านฐาน ( BหรือC) [ 1 ] [ 2 ]
ใช้เมื่อการสืบทอดแสดงถึงการจำกัดเซตมากกว่าการประกอบส่วนต่างๆ ในภาษา C++ คลาสพื้นฐานที่ตั้งใจให้ใช้ร่วมกันตลอดทั้งลำดับชั้นจะถูกระบุว่าเป็นคลาสเสมือนโดยใช้virtualคำหลัก ` virtual`
พิจารณาโครงสร้างลำดับชั้นของคลาสต่อไปนี้
class Animal { public : virtual ~ Animal () = default ; // แสดงให้เห็นอย่างชัดเจนว่าตัวทำลายคลาสเริ่มต้นจะถูกเรียกใช้virtual void eat () {} };คลาสMammal : public Animal { public : virtual void breathe () {} };class WingedAnimal : public Animal { public : virtual void flap () {} };// ค้างคาวเป็นสัตว์เลี้ยงลูกด้วยนมมีปีกคลาสBat : public Mammal , public WingedAnimal {};ดังที่กล่าวไว้ข้างต้น การเรียกใช้เมธอดนั้นbat.eat()มีความกำกวม เนื่องจากมีAnimalคลาสพื้นฐาน (ทางอ้อม) สองคลาสใน เมธอดนั้น Batดังนั้นBatอ็อบเจ็กต์ใดๆ ก็ตามจะมีซับอ็อบเจ็กต์คลาสพื้นฐานที่แตกต่างกันสองคลาสAnimalดังนั้น การพยายามผูกการอ้างอิงโดยตรงกับAnimalซับอ็อบเจ็กต์ของBatอ็อบเจ็กต์จะล้มเหลว เนื่องจากความสัมพันธ์ดังกล่าวมีความกำกวมโดยเนื้อแท้
Bat bat ; Animal & animal = bat ; // ข้อผิดพลาด: ควรแปลง Bat เป็นซับออบเจ็กต์ Animal ใด// Mammal::Animal หรือ WingedAnimal::Animal?เพื่อให้เกิดความชัดเจน จำเป็นต้องแปลงbatเป็นซับออบเจ็กต์ของคลาสพื้นฐานใดคลาสหนึ่งอย่างชัดเจน:
ค้างคาวค้างคาว; สัตว์& สัตว์เลี้ยงลูกด้วยนม= static_cast < สัตว์เลี้ยงลูกด้วยนม&> ( ค้างคาว); สัตว์& มีปีก= static_cast < สัตว์มีปีก&> ( ค้างคาว);ในการเรียกใช้eat()จำเป็นต้องมีการแยกแยะความหมายหรือการระบุคุณสมบัติอย่างชัดเจนเช่นเดียวกัน: static_cast<Mammal&>(bat).eat()หรือstatic_cast<WingedAnimal&>(bat).eat()หรืออีกทางเลือกหนึ่งคือbat.Mammal::eat()และbat.WingedAnimal::eat()การระบุคุณสมบัติอย่างชัดเจนไม่เพียงแต่ใช้ไวยากรณ์ที่ง่ายและสม่ำเสมอกว่าสำหรับทั้งพอยเตอร์และออบเจ็กต์เท่านั้น แต่ยังช่วยให้สามารถเรียกใช้แบบคงที่ได้ด้วย ดังนั้นจึงอาจกล่าวได้ว่าเป็นวิธีการที่เหมาะสมกว่า
ในกรณีนี้ การสืบทอดแบบสองชั้นAnimalอาจไม่เป็นที่ต้องการ เนื่องจากเราต้องการจำลองว่าความสัมพันธ์ ( Batเป็นAnimal) มีอยู่เพียงครั้งเดียวเท่านั้น การที่ a Batเป็น a Mammalและ a เป็นWingedAnimalไม่ได้หมายความว่ามันเป็น a Animalสองครั้ง: Animalคลาสพื้นฐานสอดคล้องกับสัญญาที่Batใช้งาน (ความสัมพันธ์ " เป็น " ข้างต้นหมายถึง " ใช้งานตามข้อกำหนดของ " จริงๆ แล้ว) และ a Batใช้งานAnimalสัญญาเพียงครั้งเดียวเท่านั้น ความหมายในโลกแห่งความเป็นจริงของ " เป็น aเพียงครั้งเดียว" คือBatควรมีวิธีการใช้งานเพียงวิธีเดียวEatไม่ใช่สองวิธีที่แตกต่างกัน ขึ้นอยู่กับว่าMammalมุมมองของBatคือ ใช้งาน หรือWingedAnimalมุมมองของBat(ในตัวอย่างโค้ดแรก เราจะเห็นว่าEatไม่ได้ถูกเขียนทับในMammalหรือWingedAnimalดังนั้นซับออบเจ็กต์ทั้งสองAnimalจะทำงานเหมือนกัน แต่เป็นเพียงกรณีที่เสื่อมสภาพ และไม่ได้สร้างความแตกต่างจากมุมมองของ C++)
สถานการณ์นี้บางครั้งเรียกว่าการสืบทอดแบบเพชร (ดูปัญหาเพชร ) เนื่องจากแผนภาพการสืบทอดมีรูปร่างคล้ายเพชร การสืบทอดเสมือนสามารถช่วยแก้ปัญหานี้ได้
วิธีแก้ปัญหา
เราสามารถประกาศคลาสของเราใหม่ได้ดังนี้:
คลาสAnimal { public : virtual ~ Animal () = default ; virtual void eat () {} };// สองคลาสที่สืบทอดคลาส Animal แบบเสมือน: class Mammal : virtual public Animal { public : virtual void breathe () {} };class WingedAnimal : virtual public Animal { public : virtual void flap () {} };// ค้างคาวก็ยังเป็นสัตว์เลี้ยงลูกด้วยนมมีปีกอยู่ดีclass Bat : public Mammal , public WingedAnimal {};ส่วนAnimalของBat::WingedAnimalตอนนี้เป็น อินสแตนซ์ เดียวกันAnimalกับที่ใช้โดยBat::Mammalซึ่งหมายความว่าBatมีเพียงอินสแตนซ์เดียวที่ใช้ร่วมกันAnimalในการแสดงผล ดังนั้นการเรียกใช้ จึงBat::Eatไม่มีความกำกวม นอกจากนี้ การแปลงโดยตรงจากBatเป็นAnimalก็ไม่มีความกำกวมเช่นกัน เนื่องจากตอนนี้มีเพียงอินAnimalสแตนซ์เดียวที่Batสามารถแปลงเป็น ได้
ความสามารถในการแชร์อินสแตนซ์เดียวของคลาสAnimalแม่ระหว่างคลาสแม่MammalและWingedAnimalคลาสลูกนั้น สามารถทำได้โดยการบันทึกค่าชดเชยหน่วยความจำระหว่าง สมาชิกใน Mammalคลาสแม่WingedAnimalและสมาชิกในคลาสฐานAnimalภายในคลาสที่สืบทอดมา อย่างไรก็ตาม โดยทั่วไปแล้ว ค่าชดเชยนี้จะทราบได้เฉพาะในขณะรันไทม์เท่านั้น ดังนั้น ค่าชดเชยจึง Batต้องเป็น ( vpointer, Mammal, vpointer, WingedAnimal, Bat, Animal) มี ตัวชี้ vtable สอง ตัว ตัวละหนึ่งตัวสำหรับลำดับชั้นการสืบทอดเสมือนที่สืบทอดจากคลาสแม่Animalในตัวอย่างนี้ ตัวหนึ่งสำหรับคลาสแม่Mammalและอีกตัวหนึ่งสำหรับคลาสลูกWingedAnimalดังนั้น ขนาดของอ็อบเจ็กต์จึงเพิ่มขึ้นสองตัวชี้ แต่ตอนนี้เหลือเพียงตัวเดียวAnimalและไม่มีความกำกวม อ็อบเจ็กต์ทั้งหมดของคลาสแม่Batจะใช้ตัวชี้ vtable เดียวกัน แต่แต่ละBatอ็อบเจ็กต์จะมีอ็อบเจ็กต์ที่ไม่ซ้ำกันของตัวเองAnimalหากคลาสอื่นสืบทอดจากMammalคลาสแม่ เช่นSquirrelคลาสลูก ตัวชี้ vtable ในMammalส่วนของคลาสแม่Squirrelโดยทั่วไปจะแตกต่างจากตัวชี้ vtable ในMammalส่วนของคลาสลูกBatแม้ว่าอาจจะเหมือนกันก็ได้หากSquirrelคลาสแม่มีขนาดเท่ากับคลาสBatแม่
ตัวอย่างเพิ่มเติมของบรรพบุรุษหลายคน
ตัวอย่างนี้แสดงให้เห็นกรณีที่คลาสพื้นฐานAมีตัวแปรคอนสตรัคเตอร์msgและมีการสืบทอดคลาสบรรพบุรุษเพิ่มเติมEจากคลาสDหลาน
เอ / \ บีซี / ดี | อี
ในที่นี้Aจะต้องสร้างขึ้นทั้งในDและEนอกจากนี้ การตรวจสอบตัวแปรmsgแสดงให้เห็นว่าคลาสกลายAเป็นคลาสพื้นฐานโดยตรงของคลาสที่สืบทอดมาจากมัน ซึ่งแตกต่างจากคลาสพื้นฐานของคลาสที่สืบทอดมาจากระดับกลางใดๆ ที่อยู่ระหว่างAและคลาสที่สืบทอดมาจากขั้นสุดท้าย
import std ;โดยใช้std :: string ;คลาสA { private : string msg ; public : explicit A ( const string & s ) : msg { s } {}void test () { std :: println ( "Hello from A: {}" , msg ); } };// B, C สืบทอดแบบเสมือนจาก A class B : virtual public A { public : B () : A ( "อินสแตนซ์ของ B" ) {} };คลาสC : virtual public A { public : C () : A ( "อินสแตนซ์ของ C" ) {} };// เนื่องจาก B และ C สืบทอด A แบบเสมือน ดังนั้น A จะต้องถูกสร้างขึ้นในแต่ละคลาสลูก// สามารถละเว้นคอนสตรัคเตอร์ B() และ C() ได้class D : public B , public C { public : D () : A ( "อินสแตนซ์ของ D" ), B (), C () {} };// สามารถละเว้นคอนสตรัคเตอร์ D() ได้class E : public D { public : E () : A ( "อินสแตนซ์ของ E" ), D () {} };// พังโดยไม่ต้องสร้าง A: // คลาส D: public B, public C { // public: // D(): // B(), C() {} // };// พังโดยไม่ต้องสร้างคลาส A // คลาส E: public D { // public: // E(): // D() {} // };int main ( int argc , char * argv []) { D d ; d . test (); // พิมพ์: "hello from A: instance of D"E e ; e . test (); // พิมพ์: "hello from A: instance of E" }วิธีการเสมือนบริสุทธิ์
สมมติว่ามีการกำหนดเมธอดเสมือนบริสุทธิ์ (pure virtual method) ไว้ในคลาสพื้นฐาน หากคลาสที่สืบทอด (deriving class) สืบทอดคลาสพื้นฐานแบบเสมือน (virtual) เมธอดเสมือนบริสุทธิ์นั้นไม่จำเป็นต้องกำหนดไว้ในคลาสที่สืบทอดนั้น อย่างไรก็ตาม หากคลาสที่สืบทอดไม่ได้สืบทอดคลาสพื้นฐานแบบเสมือน เมธอดเสมือนทั้งหมดจะต้องถูกกำหนดไว้ในคลาสที่สืบทอดนั้น
import std ;โดยใช้std :: string ;คลาสA { protected : string msg ; public : explicit A ( const string & s ) : msg { s } {}void test () { std :: println ( "Hello from A: {}" , msg ); } virtual void pureVirtualTest () = 0 ; };// เนื่องจาก B และ C สืบทอด A แบบเสมือน ดังนั้นจึงไม่จำเป็นต้องกำหนดเมธอด purevirtualTest class B : virtual public A { public : explicit B ([[ maybe_unused ]] const string & s = "" ) : A ( "instance of B" ) {} };class C : virtual public A { public : explicit C ([[ maybe_unused ]] const string & s = "" ) : A ( "instance of C" ) {} };// เนื่องจาก B และ C สืบทอด A แบบเสมือน ดังนั้น A จะต้องถูกสร้างขึ้นในแต่ละคลาสลูก// อย่างไรก็ตาม เนื่องจาก D ไม่ได้สืบทอด B และ C แบบเสมือน ดังนั้นเมธอดเสมือนบริสุทธิ์ใน A *จะต้องถูกกำหนด* คลาสD : public B , public C { public : explicit D ([[ maybe_unused ]] const string & s = "" ) : A ( "อินสแตนซ์ของ D จากคอนสตรัคเตอร์ A" ), B ( "อินสแตนซ์ของ D จากคอนสตรัคเตอร์ B" ), C ( "อินสแตนซ์ของ D จากคอนสตรัคเตอร์ C" ) {}void pureVirtualTest () override { std :: println ( "Pure virtual hello from: {}" , msg ); } };// ไม่จำเป็นต้องกำหนดนิยามใหม่ของเมธอดเสมือนบริสุทธิ์หลังจากที่คลาสแม่กำหนดไว้แล้วคลาสE : public D { public : explicit E ([[ maybe_unused ]] const string & s = "" ) : A ( "อินสแตนซ์ของ E จากคอนสตรัคเตอร์ A" ), D ( "อินสแตนซ์ของ E จากคอนสตรัคเตอร์ D" ) {} };int main ( int argc , char * argv []) { D d ( "d" ); d . test (); // Hello from A: instance of D from constructor A d . pureVirtualTest (); // Pure virtual hello from: instance of D from constructor AE e ( "e" ); e . test (); // สวัสดีจาก A: อินสแตนซ์ของ E จากคอนสตรัคเตอร์ A e . pureVirtualTest (); // สวัสดีเสมือนบริสุทธิ์จาก: อินสแตนซ์ของ E จากคอนสตรัคเตอร์ A }สรุปเนื้อหา
ข้อมูลสำคัญจากบทความ
ข้อมูลสำคัญเกี่ยวกับ มรดกเสมือนจริง
การสืบทอดเสมือน (Virtual inheritance ) เป็น เทคนิคใน ภาษา C++ที่ช่วยให้มั่นใจได้ว่า คลาสที่สืบทอดมาจาก คลาสพื้นฐาน จะ ได้รับสำเนา ของ ตัวแปรสมาชิกเพียง ชุดเดียวเท่านั้น...
ตัวอย่างเพิ่มเติมของบรรพบุรุษหลายคน
ตัวอย่างนี้แสดงให้เห็นกรณีที่คลาสพื้นฐาน A มีตัวแปรคอนสตรัคเตอร์ msg และมีการสืบทอดคลาสบรรพบุรุษเพิ่มเติม E จากคลาส D หลาน