กลับไปหน้าบทความ

อ่าน 5 นาที

ตารางวิธีเสมือน

ใน การเขียนโปรแกรมคอมพิวเตอร์ ตาราง เมธอดเสมือน ( VMT ), ตารางฟังก์ชันเสมือน , ตารางการเรียกเสมือน , ตารางการส่งคำสั่ง , vtable หรือ vftable คือกลไกที่ใช้ใน ภาษาโปรแกรม...

ตารางวิธีเสมือน

ในการเขียนโปรแกรมคอมพิวเตอร์ตารางเมธอดเสมือน ( VMT ), ตารางฟังก์ชันเสมือน , ตารางการเรียกเสมือน , ตารางการส่งคำสั่ง , vtableหรือvftableคือกลไกที่ใช้ในภาษาโปรแกรมเพื่อรองรับการส่งคำสั่งแบบไดนามิก (หรือการผูกเมธอดขณะรันไทม์ )

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

มีหลายวิธีในการนำการส่งแบบไดนามิกดังกล่าวมาใช้ แต่การใช้ตารางเมธอดเสมือนเป็นเรื่องปกติโดยเฉพาะใน ภาษา C++และภาษาที่เกี่ยวข้อง (เช่นDและC# ) ภาษาที่แยกอินเทอร์เฟซการเขียนโปรแกรมของวัตถุออกจากการใช้งาน เช่นVisual BasicและDelphiก็มักจะใช้วิธีนี้เช่นกัน เพราะวิธีนี้ช่วยให้วัตถุสามารถใช้การใช้งานที่แตกต่างกันได้เพียงแค่ใช้ชุดตัวชี้เมธอดที่แตกต่างกัน วิธีนี้ช่วยให้สามารถสร้างไลบรารีภายนอกได้ ในขณะที่เทคนิคอื่นๆ อาจทำไม่ได้[ 1 ]

สมมติว่าโปรแกรมประกอบด้วยคลาสสามคลาสในลำดับชั้นการสืบทอด: คลาสแม่ , , และ คลาสย่อยCatสอง คลาส , และคลาสCatกำหนดฟังก์ชันเสมือนชื่อดังนั้นคลาสย่อยของมันอาจมีการใช้งานที่เหมาะสม (เช่นหรือ) เมื่อโปรแกรมเรียก ฟังก์ชัน speakบน การอ้างอิง Cat (ซึ่งอาจอ้างอิงถึงอินสแตนซ์ของหรืออินสแตนซ์ของหรือ) โค้ดจะต้องสามารถระบุได้ว่าควรส่ง การเรียก ไปยังการใช้งานใดของฟังก์ชัน ซึ่งขึ้นอยู่กับคลาสจริงของวัตถุ ไม่ใช่คลาสของการอ้างอิงถึงมัน ( ) โดยทั่วไปแล้วไม่สามารถระบุคลาสได้แบบคงที่ (นั่นคือ ในเวลาคอมไพล์ ) ดังนั้นคอมไพเลอร์จึงไม่สามารถตัดสินใจได้ว่าจะเรียกฟังก์ชันใดในเวลานั้น การเรียกจะต้องถูกส่งไปยังฟังก์ชันที่ถูกต้องแบบไดนามิก (นั่นคือ ในเวลาทำงาน ) แทน HouseCatLionspeak()meow()roar()CatHouseCatLionCat

การดำเนินการ

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

มาตรฐานC++ไม่ได้กำหนดวิธีการใช้งานการเรียกใช้ฟังก์ชันแบบไดนามิกอย่างแน่ชัด แต่โดยทั่วไปแล้วคอมไพเลอร์จะใช้รูปแบบพื้นฐานเดียวกันแต่มีการปรับเปลี่ยนเล็กน้อย

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

คอมไพเลอร์หลายตัววางตัวชี้ตารางเสมือนไว้ที่สมาชิกตัวสุดท้ายของออบเจ็กต์ ในขณะที่คอมไพเลอร์บางตัววางไว้ที่สมาชิกตัวแรก โค้ดต้นฉบับที่พกพาได้ทำงานได้ทั้งสองแบบ[ 3 ] ตัวอย่างเช่นg++เคยวางตัวชี้ไว้ที่ส่วนท้ายของออบเจ็กต์[ 4 ]

ตัวอย่าง

พิจารณาการประกาศคลาสต่อไปนี้ในภาษา C++ :

import std ;คลาสBase1 { private : int b1 = 0 ; public : explicit Base1 ( int b1 ) : b1 { b1 } {}virtual ~ Base1 () = default ;void nonVirtual () { std :: println ( "Base1::nonVirtual() ถูกเรียกแล้ว!" ); }virtual void fn1 () { std :: println ( "Base1::fn1() ถูกเรียกแล้ว!" ); } };คลาสBase2 { private : int b2 = 0 ; public : explicit Base2 ( int b2 ) : b2 { b2 } {}virtual ~ Base2 () = default ;virtual void fn2 () { std :: println ( "Base2::fn2() ถูกเรียกแล้ว!" ); } };class Derived : public Base1 , public Base2 { private : int d = 0 ; public : explicit Base1 ( int b1 , int b2 , int d ) : Base1 ( b1 ), Base2 ( b2 ), d { d } {}~ อนุพันธ์() = ค่าเริ่มต้น;void fn3 () { std :: println ( "Derived::fn3() called!" ); }void fn2 () override { std :: println ( "Derived::fn2() called!" ); } };int main () { Base2 * base2 = new Base2 (); Derived * derived = new Derived ();// ...ลบbase2 ; ลบderived ; }

g++ 3.4.6 จากGCCสร้างโครงสร้างหน่วยความจำ 32 บิตสำหรับวัตถุดังต่อไปนี้base2: [ nb 1 ]

b2: +0: ​​ตัวชี้ไปยังตารางเมธอดเสมือนของ Base2 +4: ค่าของ b2 ตารางวิธีเสมือนของฐาน 2: +0: ​​Base2::fn2() 

และโครงสร้างหน่วยความจำต่อไปนี้สำหรับวัตถุนั้นderived:

ที่มา: +0: ​​ตัวชี้ไปยังตารางเมธอดเสมือนของคลาสที่ได้มา (สำหรับ Base1) +4: ค่าของ b1 +8: ตัวชี้ไปยังตารางเมธอดเสมือนของ Derived (สำหรับ Base2) +12: ค่าของ b2 +16: ค่าของ d ขนาดรวม: 20 ไบต์ ตารางวิธีเสมือนของอนุพันธ์ (สำหรับฐาน 1): +0: ​​Base1::fn1() // Base1::fn1() ไม่ได้ถูกเขียนทับ ตารางวิธีเสมือนของอนุพันธ์ (สำหรับฐาน 2): +0: ​​Derived::fn2() // Base2::fn2() ถูกเขียนทับโดย Derived::fn2() 

โปรดทราบว่าฟังก์ชันที่ไม่มีคีย์เวิร์ดvirtualในคำประกาศ (เช่นnonVirtual()และd()) โดยทั่วไปจะไม่ปรากฏในตารางเมธอดเสมือน ยกเว้นในกรณีพิเศษตามที่กำหนดโดย คอนสตรัค เตอร์ เริ่มต้น

โปรดสังเกตตัวทำลายเสมือนในคลาสพื้นฐาน ด้วย Base1และBase2สิ่งเหล่านี้จำเป็นเพื่อให้แน่ใจว่าdelete derived;สามารถปลดปล่อยหน่วยความจำได้ไม่เพียงแค่สำหรับDerivedแต่ยังรวมถึงBase1และ ด้วย Base2หากderivedเป็นตัวชี้หรือการอ้างอิงไปยังประเภทBase1หรือB2สิ่งเหล่านี้ถูกยกเว้นจากโครงสร้างหน่วยความจำเพื่อให้ตัวอย่างง่ายขึ้น[ nb 2 ]

การเขียนทับเมธอดfn2()ในคลาสDerivedจะทำได้โดยการคัดลอกตารางเมธอดเสมือนของBase2และแทนที่ตัวชี้ไปยังBase2::fn2()ด้วยตัวชี้ไปDerived::fn2()ยัง

การสืบทอดแบบหลายทางและธังค์

คอมไพเลอร์ g++ ใช้การสืบทอดแบบหลายทางของคลาสBase1และBase2ภายในคลาสDerivedโดยใช้ตารางเมธอดเสมือนสองตาราง ตารางละหนึ่งตารางสำหรับคลาสพื้นฐานแต่ละคลาส (มีวิธีอื่นในการใช้การสืบทอดแบบหลายทาง แต่เป็นวิธีที่ใช้กันทั่วไป) ซึ่งนำไปสู่ความจำเป็นต้องใช้ "การแก้ไขตัวชี้" หรือที่เรียกว่าthunkเมื่อ ทำการ แปลง ประเภทข้อมูล

พิจารณาโค้ด C++ ต่อไปนี้:

Derived * derived = new Derived (); Base1 * base1 = derived ; Base2 * base2 = derived ;

ในขณะที่derivedและbase1จะชี้ไปยังตำแหน่งหน่วยความจำเดียวกันหลังจากดำเนินการโค้ดนี้แล้วbase2จะชี้ไปยังตำแหน่งderived + 8(แปดไบต์ถัดจากตำแหน่งหน่วยความจำของderived) ดังนั้น จึงbase2ชี้ไปยังบริเวณภายในderivedที่ "ดูเหมือน" อินสแตนซ์ของBase2กล่าวคือ มีโครงสร้างหน่วยความจำเหมือนกับอินสแตนซ์Base2ของ

การอธิษฐาน

การเรียกใช้เมธอดนั้นderived->fn1()จะดำเนินการโดยการเข้าถึงค่าที่ชี้โดย vpointer derivedของ เมธอดนั้น Derived::Base1ค้นหาfn1ข้อมูลในตารางเมธอดเสมือน แล้วจึงเข้าถึงค่าที่ชี้โดยพอยเตอร์นั้นเพื่อเรียกใช้โค้ด

การสืบทอดทางเดียว

ในกรณีของการสืบทอดแบบทางเดียว (หรือในภาษาที่มีการสืบทอดแบบทางเดียวเท่านั้น) หาก vpointer เป็นองค์ประกอบแรกเสมอderived(เช่นเดียวกับในคอมไพเลอร์หลายตัว) โค้ดจะลดรูปเป็น pseudo-C++ ดังต่อไปนี้:

( * (( * ที่ได้มาจาก)[ 0 ]))( ที่ได้มาจาก)

โดยที่*derivedหมายถึงตารางเมธอดเสมือนของDerivedและ[0]หมายถึงเมธอดแรกในตารางเมธอดเสมือน พารามิเตอร์derivedจะกลายเป็น" this" ตัวชี้ไปยังอ็อบเจ็กต์

การสืบทอดทางพันธุกรรมหลายทาง

ในกรณีทั่วไป การเรียกขานBase1::fn1()หรือDerived::fn2()นั้นมีความซับซ้อนกว่า:

// เรียก derived->fn1() ( * ( * ( derived [ 0 ] /*ตัวชี้ไปยังตารางเมธอดเสมือนของ Derived (สำหรับ Base1)*/ )[ 0 ]))( derived )// เรียก derived->fn2() ( * ( * ( derived [ 8 ] /*ตัวชี้ไปยังตารางเมธอดเสมือนของ Derived (สำหรับ Base2)*/ )[ 0 ]))( derived + 8 )

การเรียกใช้ ฟังก์ชัน derived->fn1()นี้ส่งBase1พอยเตอร์เป็นพารามิเตอร์ การเรียกใช้ ฟังก์ชันนี้ derived->fn2()ส่งBase2พอยเตอร์เป็นพารามิเตอร์ การเรียกใช้ครั้งที่สองนี้ต้องมีการแก้ไขเพื่อให้ได้พอยเตอร์ที่ถูกต้อง ตำแหน่งของเมธอดนี้Base2::fn2ไม่ได้อยู่ในตารางเมธอดเสมือนของเมธอดDerivedนั้น

เมื่อเปรียบเทียบกันแล้ว การโทรไปยังหมายเลขderived->fnonvirtual()นั้นง่ายกว่ามาก:

( * Base1 :: fnonvirtual )( derived )

ประสิทธิภาพ

การเรียกฟังก์ชันเสมือนต้องใช้การอ้างอิงดัชนีเพิ่มเติมอย่างน้อยหนึ่งครั้ง และบางครั้งอาจต้องมีการเพิ่ม "การแก้ไข" เมื่อเทียบกับการเรียกฟังก์ชันที่ไม่ใช่เสมือน ซึ่งเป็นเพียงการกระโดดไปยังตัวชี้ที่คอมไพล์ไว้ ดังนั้น การเรียกฟังก์ชันเสมือนจึงช้ากว่าการเรียกฟังก์ชันที่ไม่ใช่เสมือนโดยธรรมชาติ การทดลองที่ทำในปี 1996 ระบุว่าเวลาการทำงานประมาณ 6–13% หมดไปกับการส่งคำสั่งไปยังฟังก์ชันที่ถูกต้อง แม้ว่าค่าใช้จ่ายเพิ่มเติมอาจสูงถึง 50% ก็ตาม[ 5 ]ต้นทุนของฟังก์ชันเสมือนอาจไม่สูงนักใน สถาปัตยกรรม CPU สมัยใหม่ เนื่องจากแคชที่ใหญ่กว่ามากและการคาดการณ์สาขาที่ ดีกว่า

นอกจากนี้ ในสภาพแวดล้อมที่ไม่ได้ใช้การคอมไพล์แบบ JIT การเรียกฟังก์ชันเสมือนมักจะไม่สามารถ แทรกเข้าไปในโค้ดได้ ในบางกรณี คอมไพเลอร์อาจสามารถดำเนินการที่เรียกว่าdevirtualizationได้ เช่น การแทนที่การค้นหาและการเรียกทางอ้อมด้วยการเรียกใช้แบบมีเงื่อนไขของแต่ละส่วนที่ถูกแทรกเข้าไป แต่การเพิ่มประสิทธิภาพในลักษณะนี้ไม่เป็นที่นิยม

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

ดังนั้น การเรียกใช้fn1ข้างต้นอาจไม่จำเป็นต้องค้นหาในตาราง เนื่องจากคอมไพเลอร์อาจบอกได้ว่าณ จุดนี้derivedสามารถเก็บได้เพียงค่า a เท่านั้น และ ไม่ได้เขียนทับเมธอดหรือคอมไพเลอร์ (หรือตัวเพิ่มประสิทธิภาพ) อาจตรวจพบว่าไม่มีคลาสย่อยของอยู่ที่ใดในโปรแกรมที่เขียนทับเมธอดการเรียกใช้หรืออาจไม่จำเป็นต้องค้นหาในตาราง เพราะมีการระบุการใช้งานไว้อย่างชัดเจน (ถึงแม้ว่าจะยังคงต้องแก้ไขตัวชี้ -pointer อยู่ก็ตาม) DerivedDerivedfn1Base1fn1Base1::fn1Base2::fn2this

การเปรียบเทียบกับทางเลือกอื่น

โดยทั่วไปแล้ว ตารางวิธีเสมือนเป็นการแลกเปลี่ยนประสิทธิภาพที่ดีเพื่อให้ได้การส่งแบบไดนามิก แต่ก็มีทางเลือกอื่น เช่นการส่งแบบต้นไม้ไบนารีซึ่งมีประสิทธิภาพสูงกว่าในบางกรณีทั่วไป แต่มีการแลกเปลี่ยนที่แตกต่างกัน[ 1 ] [ 6 ]

อย่างไรก็ตาม ตารางเมธอดเสมือนอนุญาตให้ส่งคำสั่งได้เพียงครั้งเดียวสำหรับพารามิเตอร์พิเศษ "this" เท่านั้น ซึ่งแตกต่างจากการส่งคำสั่งหลายครั้ง (เช่นในCLOS , DylanหรือJulia ) ที่สามารถนำประเภทของพารามิเตอร์ทั้งหมดมาพิจารณาในการส่งคำสั่งได้

ตารางเมธอดเสมือนจะใช้งานได้ก็ต่อเมื่อการเรียกใช้เมธอดถูกจำกัดไว้เฉพาะชุดเมธอดที่ทราบเท่านั้น เพื่อให้สามารถจัดวางไว้ในอาร์เรย์แบบง่ายที่สร้างขึ้นในระหว่างการคอมไพล์ ซึ่งแตกต่างจาก ภาษา แบบ Duck Typing (เช่นSmalltalk , PythonหรือJavaScript )

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

ดูเพิ่มเติม

หมายเหตุ

  1. ^ อาร์กิวเมนต์ ของ G++-fdump-class-hierarchy(เริ่มตั้งแต่เวอร์ชัน 8:-fdump-lang-class) สามารถใช้เพื่อดัมพ์ตารางเมธอดเสมือนเพื่อตรวจสอบด้วยตนเอง สำหรับคอมไพเลอร์ AIX VisualAge XlC ให้ใช้-qdump_class_hierarchyเพื่อแสดงลำดับชั้นของคลาสและโครงสร้างตารางฟังก์ชันเสมือน
  2. ^ "C++ - เหตุใดจึงมีตัวทำลายเสมือนสองตัวในตารางเสมือน และที่อยู่ของฟังก์ชันที่ไม่ใช่เสมือนอยู่ที่ไหน (gcc4.6.3) "
ดึงข้อมูลมาจาก " https://en.wikipedia.org/w/index.php?title=Virtual_method_table&oldid=1327861334 "

สรุปเนื้อหา

ข้อมูลสำคัญจากบทความ

ข้อมูลสำคัญเกี่ยวกับ ตารางวิธีเสมือน

ใน การเขียนโปรแกรมคอมพิวเตอร์ ตาราง เมธอดเสมือน ( VMT ), ตารางฟังก์ชันเสมือน , ตารางการเรียกเสมือน , ตารางการส่งคำสั่ง , vtable หรือ vftable คือกลไกที่ใช้ใน ภาษาโปรแกรม...

การดำเนินการ

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

ตัวอย่าง

พิจารณาการประกาศคลาสต่อไปนี้ใน ภาษา C++ :

การสืบทอดแบบหลายทางและธังค์

คอมไพเลอร์ g++ ใช้ การสืบทอดแบบหลายทาง ของคลาส Base1 และ Base2 ภายในคลาส Derived โดยใช้ตารางเมธอดเสมือนสองตาราง ตารางละหนึ่งตารางสำหรับคลาสพื้นฐานแต่ละคลาส (มีวิธีอื่นในการใช้การสืบทอดแบบหลายทาง แต่เป็นวิธีที่ใช้กันทั่วไป) ซึ่งนำไปสู่ความจำเป็นต้องใช้...