เหตุใดจึงนิยมใช้ 'ความแตกต่างแบบบริสุทธิ์' มากกว่าการใช้ RTTI


106

ทรัพยากร C ++ เกือบทั้งหมดที่ฉันเคยเห็นที่พูดถึงเรื่องประเภทนี้บอกฉันว่าฉันควรใช้วิธีการหลายรูปแบบในการใช้ RTTI (การระบุประเภทรันไทม์) โดยทั่วไปแล้วฉันให้คำแนะนำแบบนี้อย่างจริงจังและจะพยายามทำความเข้าใจกับเหตุผล - ท้ายที่สุด C ++ เป็นสัตว์ร้ายที่ยิ่งใหญ่และยากที่จะเข้าใจในเชิงลึกทั้งหมด อย่างไรก็ตามสำหรับคำถามเฉพาะนี้ฉันกำลังวาดภาพว่างและต้องการดูว่าอินเทอร์เน็ตสามารถให้คำแนะนำประเภทใดได้บ้าง ก่อนอื่นให้ฉันสรุปสิ่งที่ฉันได้เรียนรู้จนถึงตอนนี้โดยระบุสาเหตุทั่วไปที่อ้างว่าทำไม RTTI จึง "ถือว่าเป็นอันตราย":

คอมไพเลอร์บางตัวไม่ใช้มัน / RTTI ไม่ได้เปิดใช้งานเสมอไป

ฉันไม่ซื้อข้อโต้แย้งนี้จริงๆ เหมือนกับการบอกว่าฉันไม่ควรใช้ฟีเจอร์ C ++ 14 เพราะมีคอมไพเลอร์ที่ไม่รองรับ แต่ก็ไม่มีใครกีดกันฉันจากการใช้คุณสมบัติ C ++ 14 โครงการส่วนใหญ่จะมีอิทธิพลเหนือคอมไพเลอร์ที่ใช้และวิธีกำหนดค่า แม้แต่การอ้างถึง manpage gcc:

-fno-rtti

ปิดใช้งานการสร้างข้อมูลเกี่ยวกับทุกคลาสด้วยฟังก์ชันเสมือนสำหรับใช้โดยคุณลักษณะการระบุชนิดรันไทม์ C ++ (dynamic_cast และ typeid) หากคุณไม่ได้ใช้ส่วนเหล่านั้นของภาษาคุณสามารถประหยัดพื้นที่ได้โดยใช้แฟล็กนี้ โปรดทราบว่าการจัดการข้อยกเว้นจะใช้ข้อมูลเดียวกัน แต่ G ++ สร้างขึ้นตามความจำเป็น ยังคงใช้ตัวดำเนินการ dynamic_cast สำหรับการแคสต์ที่ไม่ต้องการข้อมูลประเภทรันไทม์เช่นการร่ายเป็น "void *" หรือคลาสพื้นฐานที่ไม่คลุมเครือ

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

มีค่าใช้จ่ายหน่วยความจำเพิ่มเติม / อาจช้า

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

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

จากคำตอบดังกล่าวดูเหมือนว่าค่าใช้จ่ายของ RTTI ยังไม่ชัดเจนเช่นกัน ต่างคนต่างวัดของต่างกัน

การออกแบบโพลีมอร์ฟิกที่หรูหราจะทำให้ RTTI ไม่จำเป็น

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

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

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

ตอนนี้โหนดของฉันอาจเป็นประเภทต่างๆ แล้วสิ่งเหล่านี้:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

นรกทำไมไม่มีแม้แต่สิ่งเหล่านี้:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

ฟังก์ชันสุดท้ายน่าสนใจ นี่คือวิธีการเขียน:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

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

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

std :: string เป็นความคิดที่ดีสำหรับประเภทหรือไม่? อาจจะไม่ แต่ฉันจะใช้อะไรได้อีก? สร้างตัวเลขและหวังว่าจะไม่มีใครใช้มัน? นอกจากนี้ในกรณีของ orange_node ของฉันฉันจะทำอย่างไรถ้าฉันต้องการใช้วิธีการจาก red_node และ yellow_node ฉันจะต้องจัดเก็บหลายประเภทต่อโหนดหรือไม่? ดูเหมือนซับซ้อน

สรุป

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

ดังนั้นกลับไปที่คำถามเดิมของฉัน: เหตุใด 'ความหลากหลายที่บริสุทธิ์' จึงดีกว่าการใช้ RTTI?


9
สิ่งที่คุณ "ขาดหายไป" (เป็นคุณลักษณะของภาษา) ในการแก้ปัญหาตัวอย่างเช่นส้มโผล่ของคุณจะเป็นการจัดส่งหลายรายการ ("หลายวิธี") ดังนั้นการมองหาวิธีการเลียนแบบที่อาจเป็นทางเลือก โดยปกติจะใช้รูปแบบผู้เยี่ยมชมดังนั้น
Daniel Jour

1
การใช้สตริงเป็นประเภทไม่เป็นประโยชน์มากนัก การใช้ตัวชี้ไปยังอินสแตนซ์ของคลาส "ประเภท" บางอย่างจะทำให้เร็วขึ้น แต่โดยพื้นฐานแล้วคุณกำลังทำสิ่งที่ RTTI ด้วยตนเอง
Daniel Jour

4
@MargaretBloom ไม่มันจะไม่ RTTI ย่อมาจากRuntime Type Information ในขณะที่ CRTP ใช้สำหรับเทมเพลตเท่านั้น - ประเภทคงที่ดังนั้น
edmz

2
@ mbr0wn: กระบวนการทางวิศวกรรมทั้งหมดถูกผูกมัดด้วยกฎเกณฑ์บางประการ การเขียนโปรแกรมไม่ใช่ข้อยกเว้น กฎสามารถแบ่งออกเป็นสองกลุ่ม: กฎอ่อน (ควร) และกฎแข็ง (ต้อง) (นอกจากนี้ยังมีคำแนะนำ / ที่เก็บข้อมูลตัวเลือก (COULD) เพื่อที่จะพูด) อ่านว่ามาตรฐาน C / C ++ (หรือมาตรฐานภาษาอังกฤษอื่น ๆ สำหรับข้อเท็จจริง) กำหนดสิ่งเหล่านี้อย่างไร ฉันเดาว่าปัญหาของคุณมาจากการที่คุณเข้าใจผิดว่า "อย่าใช้ RTTI" เป็นกฎที่ยาก ("คุณไม่ได้ใช้ RTTI") จริงๆแล้วมันเป็นกฎที่นุ่มนวล ("คุณไม่ควรใช้ RTTI") ซึ่งหมายความว่าคุณควรหลีกเลี่ยงเมื่อใดก็ตามที่เป็นไปได้ - และใช้เมื่อคุณไม่สามารถหลีกเลี่ยงการทำเช่นนั้นได้

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

คำตอบ:


69

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

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

ตอนนี้ทุกคนที่จับคู่กับคู่รัก "ส้ม" กับส้มทั้งชนิดและโดยนัยกับสวนส่วนตัวของคุณทั้งหมดแทนที่จะใช้อินเทอร์เฟซที่กำหนดไว้

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

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

class node_base {
  public:
    bool has_tag(tag_name);

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

สิ่งนี้สามารถขยายไปยังวิธีการแบบไดนามิกได้หากคุณต้องการ "คุณสนับสนุนให้ Foo มีปากเสียงกับ Baz, Tom and Alice หรือไม่ตกลง Fooing คุณ" ในแง่ใหญ่นี่เป็นการรบกวนน้อยกว่าการร่ายแบบไดนามิกที่จะได้รับข้อเท็จจริงว่าวัตถุอื่น ๆ เป็นประเภทที่คุณรู้จัก

ตอนนี้วัตถุส้มเขียวหวานสามารถมีแท็กสีส้มและเล่นได้ในขณะที่กำลังแยกการใช้งาน

มันยังคงนำไปสู่ความยุ่งเหยิง แต่อย่างน้อยก็เป็นเรื่องยุ่งเหยิงของข้อความและข้อมูลไม่ใช่ลำดับชั้นการนำไปใช้งาน

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


14
+1 สำหรับย่อหน้าสุดท้าย ไม่ใช่แค่เพราะฉันเห็นด้วยกับคุณ แต่เป็นเพราะค้อนบนเล็บที่นี่

7
เราจะได้รับฟังก์ชันการทำงานเฉพาะอย่างไรเมื่อรู้ว่ามีการแท็กวัตถุว่าสนับสนุนฟังก์ชันนั้น สิ่งนี้เกี่ยวข้องกับการคัดเลือกนักแสดงหรือมีคลาสพระเจ้าที่มีฟังก์ชันของสมาชิกทุกคนที่เป็นไปได้ ความเป็นไปได้แรกคือการแคสต์ที่ไม่ถูกตรวจสอบซึ่งในกรณีนี้การติดแท็กเป็นเพียงรูปแบบการตรวจสอบประเภทไดนามิกที่ผิดพลาดหรือถูกตรวจสอบdynamic_cast(RTTI) ซึ่งในกรณีนี้แท็กซ้ำซ้อน ความเป็นไปได้ที่สองซึ่งเป็นชนชั้นพระเจ้าเป็นที่น่ารังเกียจ สรุปแล้วคำตอบนี้มีหลายคำที่ฉันคิดว่าฟังดูดีสำหรับโปรแกรมเมอร์ Java แต่เนื้อหาจริงไม่มีความหมาย
ไชโยและ hth - Alf

2
@ Falco: นั่นคือ (ตัวแปรหนึ่งของ) ความเป็นไปได้แรกที่ฉันพูดถึงการคัดเลือกที่ไม่ได้เลือกตามแท็ก การติดแท็กเป็นรูปแบบการตรวจสอบประเภทไดนามิกที่เปราะบางและผิดพลาดได้ง่ายมาก การทำงานที่ไม่เหมาะสมของรหัสไคลเอ็นต์เล็กน้อยและใน C ++ หนึ่งปิดอยู่ใน UB-land คุณไม่ได้รับข้อยกเว้นเช่นเดียวกับที่คุณอาจได้รับใน Java แต่พฤติกรรมที่ไม่ได้กำหนดเช่นข้อขัดข้องและ / หรือผลลัพธ์ที่ไม่ถูกต้อง นอกจากจะไม่น่าเชื่อถือและอันตรายแล้วยังไม่มีประสิทธิภาพอย่างมากเมื่อเทียบกับรหัส C ++ ที่มีเหตุผลมากกว่า IOW. มันไม่ดีมาก มาก
ไชโยและ hth - Alf

1
อืม. :) ประเภทอาร์กิวเมนต์?
ไชโยและ hth - Alf

2
@JojOatXGME: เนื่องจาก "ความหลากหลาย" หมายถึงความสามารถในการทำงานกับหลากหลายประเภท หากคุณต้องตรวจสอบว่าเป็นประเภทใดประเภทหนึ่งนอกเหนือจากการตรวจสอบประเภทที่มีอยู่แล้วที่คุณใช้เพื่อเริ่มต้นด้วยตัวชี้ / การอ้างอิงแสดงว่าคุณกำลังมองหาความหลากหลาย คุณไม่ได้ทำงานกับประเภทต่างๆ คุณกำลังทำงานกับประเภทเฉพาะ ใช่มี "โครงการ (ใหญ่) ใน Java" ที่ทำเช่นนี้ แต่นั่นJava ; ภาษาอนุญาตเฉพาะความหลากหลายแบบไดนามิก C ++ มีความหลากหลายแบบคงที่เช่นกัน นอกจากนี้เพียงเพราะใครบางคน "ใหญ่" มันไม่ได้ทำให้เป็นความคิดที่ดี
Nicol Bolas

31

ความสุภาพทางศีลธรรมส่วนใหญ่ต่อสิ่งนี้หรือคุณลักษณะนั้นเป็นเรื่องปกติที่เกิดจากการสังเกตว่ามีการใช้คุณลักษณะนั้นอย่างเข้าใจผิดจำนวนมาก

ในกรณีที่นักศีลธรรมล้มเหลวคือพวกเขาคิดว่าการใช้งานทั้งหมดมีความเข้าใจผิดในขณะที่ในความเป็นจริงมีอยู่ด้วยเหตุผล

พวกเขามีสิ่งที่ผมเคยเรียกว่า "ช่างประปาที่ซับซ้อน" พวกเขาคิดว่าทุกก๊อกมีการชำรุดเพราะทุกก๊อกน้ำที่พวกเขาถูกเรียกให้ซ่อมแซม ความจริงก็คือก๊อกส่วนใหญ่ใช้งานได้ดี: คุณไม่เรียกช่างประปาให้พวกเขา!

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

IF(selection) CALL(something) WITH(parameters)มีวิธีการทั่วไปที่จะคิดเกี่ยวกับความแตกต่างคือ (ขออภัย แต่การเขียนโปรแกรมเมื่อไม่คำนึงถึงสิ่งที่เป็นนามธรรมเป็นเรื่องเกี่ยวกับเรื่องนั้น)

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

แนวคิดก็คือ:

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

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

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

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

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

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

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

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

นั่นคือสิ่งที่ศีลธรรมผลักดันให้หลีกเลี่ยง แต่นั่นไม่ได้หมายความว่าจะไม่มีปัญหาที่อาศัยอยู่ในโดเมนล่างสุด!

การทุบตี RTTI ก็เหมือนกับการทุบตีgotoเพียงเพื่อทุบตี สิ่งสำหรับนกแก้วไม่ใช่โปรแกรมเมอร์


บัญชีที่ดีของระดับที่แต่ละแนวทางสามารถใช้ได้ ฉันไม่เคยได้ยินคำว่า "Goedelization" - เป็นที่รู้จักในชื่ออื่นหรือไม่? คุณช่วยเพิ่มลิงค์หรือคำอธิบายเพิ่มเติมได้ไหม ขอบคุณ :)
j_random_hacker

1
@j_random_hacker: ฉันก็อยากรู้เกี่ยวกับการใช้ Godelization นี้เหมือนกัน โดยปกติคนหนึ่งจะนึกถึง Godelization เป็นอันดับแรกการแมปจากสตริงบางส่วนไปเป็นจำนวนเต็มและอย่างที่สองใช้เทคนิคนี้เพื่อสร้างข้อความอ้างอิงตัวเองในภาษาทางการ ฉันไม่คุ้นเคยกับคำนี้ในบริบทของการจัดส่งเสมือนจริงและชอบที่จะเรียนรู้เพิ่มเติม
Eric Lippert

1
ในความเป็นจริงฉันใช้คำในทางที่ผิด : อ้างอิงจาก Goedle เนื่องจากจำนวนเต็มทุกจำนวนตรงกับจำนวนเต็ม n-ple (พลังของปัจจัยที่สำคัญ) และทุก n-ple จะสอดคล้องกับจำนวนเต็มปัญหาการจัดทำดัชนี n มิติที่ไม่ต่อเนื่องทุกปัญหาสามารถเป็นได้ ลดลงเหลือเพียงมิติเดียว นั่นไม่ได้หมายความว่านี่เป็นวิธีเดียวเท่านั้นที่จะทำได้มันเป็นเพียงวิธีที่บอกว่า "เป็นไปได้" สิ่งที่คุณต้องมีคือกลไก "แบ่งและพิชิต" ฟังก์ชันเสมือนคือ "การแบ่ง" และการสืบทอดหลายรายการคือ "พิชิต"
Emilio Garavaglia

... เมื่อสิ่งที่เกิดขึ้นภายในชุดค่าผสมเชิงเส้นจำกัด (ช่วง) มีประสิทธิภาพมากขึ้น (คลาส i = r * C + c รับดัชนีในอาร์เรย์ของเซลล์ของเมทริกซ์) ในกรณีนี้รหัสหาร "ผู้เยี่ยมชม" และผู้พิชิตคือ "คอมโพสิต" เนื่องจากพีชคณิตเชิงเส้นเกี่ยวข้องเทคนิคในกรณีนี้จึงสอดคล้องกับ "diagonalization"
Emilio Garavaglia

อย่าคิดว่าทั้งหมดนี้เป็นเทคนิค พวกเขาเป็นเพียงการเปรียบเทียบ
Emilio Garavaglia

23

มันดูเรียบร้อยในตัวอย่างเล็ก ๆ แต่ในชีวิตจริงในไม่ช้าคุณจะต้องเจอกับประเภทยาว ๆ ที่สามารถกระตุ้นซึ่งกันและกันได้บางคนอาจจะอยู่ในทิศทางเดียวเท่านั้น

สิ่งที่เกี่ยวกับdark_orange_nodeหรือblack_and_orange_striped_nodeหรือdotted_node? มันมีจุดสีต่างกันได้ไหม? จะเป็นอย่างไรถ้าจุดส่วนใหญ่เป็นสีส้มสามารถจิ้มได้หรือไม่?

และทุกครั้งที่คุณต้องเพิ่มกฎใหม่คุณจะต้องทบทวนpoke_adjacentฟังก์ชันทั้งหมดและเพิ่ม if-statement เพิ่มเติม


เช่นเคยเป็นเรื่องยากที่จะสร้างตัวอย่างทั่วไปฉันจะให้ข้อมูลนั้น

แต่ถ้าฉันจะทำตัวอย่างเฉพาะนี้ฉันจะเพิ่มpoke()สมาชิกในชั้นเรียนทั้งหมดและปล่อยให้บางคนไม่สนใจการโทร ( void poke() {}) หากพวกเขาไม่สนใจ

แน่นอนว่ามันจะแพงกว่าการเปรียบเทียบtypeids ด้วยซ้ำ


3
คุณพูดว่า "แน่นอน" แต่อะไรทำให้คุณมั่นใจได้ นั่นคือสิ่งที่ฉันพยายามคิดจริงๆ สมมติว่าฉันเปลี่ยนชื่อ orange_node เป็น pokable_node และเป็นคนเดียวที่ฉันสามารถเรียก poke () ได้ นั่นหมายความว่าอินเทอร์เฟซของฉันจะต้องใช้เมธอด poke () ที่พูดว่าพ่นข้อยกเว้น ("โหนดนี้ไม่สามารถ pokable") ที่ดูเหมือนว่ามากขึ้นมีราคาแพง
mbr0wn

2
ทำไมเขาต้องโยนข้อยกเว้น? หากคุณสนใจว่าอินเทอร์เฟซนั้น "poke-able" หรือไม่เพียงแค่เพิ่มฟังก์ชัน "isPokeable" และเรียกใช้งานก่อนที่จะเรียกใช้ฟังก์ชัน poke หรือเพียงแค่ทำตามที่เขาพูดและ "ไม่ทำอะไรเลยในชั้นเรียนที่ไม่โผล่ออกมา"
Brandon

1
@ mbr0wn: คำถามที่ดีกว่าคือทำไมคุณถึงต้องการให้โหนด pokable และ nonpokable แชร์คลาสฐานเดียวกัน
Nicol Bolas

2
@NicolBolas ทำไมคุณถึงต้องการให้มอนสเตอร์ที่เป็นมิตรและไม่เป็นมิตรแบ่งปันคลาสพื้นฐานเดียวกันหรือองค์ประกอบ UI ที่โฟกัสได้และไม่สามารถโฟกัสได้หรือคีย์บอร์ดที่มีแป้นตัวเลขและคีย์บอร์ดที่ไม่มีตัวเลข
user253751

1
@ mbr0wn ฟังดูเหมือนพฤติกรรม - แบบแผน อินเทอร์เฟซพื้นฐานมีสองวิธีsupportsBehaviourและinvokeBehaviourแต่ละคลาสสามารถมีรายการพฤติกรรมได้ พฤติกรรมหนึ่งจะเป็น Poke และสามารถเพิ่มลงในรายการพฤติกรรมที่รองรับได้โดยทุกคลาสที่ต้องการให้เป็น Pokeable
Falco

20

คอมไพเลอร์บางตัวไม่ใช้มัน / RTTI ไม่ได้เปิดใช้งานเสมอไป

ฉันเชื่อว่าคุณเข้าใจข้อโต้แย้งดังกล่าวผิด

มีสถานที่เข้ารหัส C ++ หลายแห่งที่ห้ามใช้ RTTI ที่สวิตช์คอมไพเลอร์ใช้เพื่อบังคับปิดใช้งาน RTTI หากคุณกำลังเขียนโค้ดภายในกระบวนทัศน์ดังกล่าว ... คุณเกือบจะได้รับแจ้งข้อ จำกัด นี้แล้ว

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

มีค่าใช้จ่ายหน่วยความจำเพิ่มเติม / อาจช้า

มีหลายสิ่งที่คุณไม่ได้ทำในฮอตลูป คุณไม่ได้จัดสรรหน่วยความจำ คุณไม่ต้องทำซ้ำผ่านรายการที่เชื่อมโยง และอื่น ๆ RTTI สามารถเป็นอีกหนึ่งสิ่งที่ "อย่าทำที่นี่" ได้อย่างแน่นอน

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

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

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

แล้ว ... ทำไมคุณถึงเขียนมันผิดวิธีเริ่มต้นด้วยล่ะ?

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

จะจบลงอย่างไร?

คุณบอกว่าคลาสพื้นฐานคือ "ลีนและมีความหมาย" แต่จริงๆ ... มันไม่มีอยู่ มันไม่ได้ทำอะไรจริง

ดูตัวอย่างของคุณ: node_base . มันคืออะไร? ดูเหมือนจะเป็นสิ่งที่มีสิ่งอื่นอยู่ติดกัน นี่คืออินเทอร์เฟซ Java (Java รุ่นก่อนทั่วไป): คลาสที่มีอยู่เพื่อเป็นสิ่งที่ผู้ใช้สามารถส่งไปยังประเภทจริงได้ บางทีคุณอาจจะเพิ่มคุณสมบัติพื้นฐานเช่น adjacency (Java เพิ่มToString) แต่ก็แค่นั้นแหละ

มีความแตกต่างระหว่าง "แบบลีนและค่าเฉลี่ย" และ "โปร่งใส"

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

แต่สิ่งที่พวกเขาทำคือทำให้การทำสิ่งใหม่ ๆ เกิดขึ้นจริงเป็นความเจ็บปวดแม้กระทั่งในระบบ พิจารณาpoke_adjacent_orangesหน้าที่ของคุณ จะเกิดอะไรขึ้นถ้ามีคนต้องการlime_nodeประเภทที่สามารถจิ้มได้เช่นเดียวกับorange_nodes? เราไม่สามารถได้มาlime_nodeจากorange_node; ที่ไม่สมเหตุสมผล

แต่เราต้องเพิ่มใหม่ที่ได้มาจากlime_node node_baseแล้วเปลี่ยนชื่อของการpoke_adjacent_oranges poke_adjacent_pokablesแล้วลองส่งไปorange_nodeและlime_node; ซึ่งไม่ว่าผลงานใดจะเป็นสิ่งที่เรากระตุ้น

อย่างไรก็ตามlime_nodeความต้องการของมันเอง poke_adjacent_pokablesและฟังก์ชันนี้จำเป็นต้องทำการตรวจสอบการหล่อแบบเดียวกัน

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

เห็นได้ชัดว่าตอนนี้คุณสร้างpoke_adjacent_pokablesฟังก์ชันฟรีเพื่อให้ใช้ได้กับทุกฟังก์ชัน แต่คุณคิดว่าจะเกิดอะไรขึ้นถ้ามีคนเพิ่มประเภทที่สี่และลืมเพิ่มลงในฟังก์ชันนั้น?

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

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

การใช้คลาสพื้นฐานแบบโปร่งใสกับ RTTI นำไปสู่ฝันร้ายในการซ่อมบำรุง อันที่จริงการใช้ RTTI ส่วนใหญ่ทำให้เกิดอาการปวดหัวในการบำรุงรักษา นั่นไม่ได้หมายความว่า RTTI ไม่มีประโยชน์ (สิ่งสำคัญสำหรับการboost::anyทำงานเป็นต้น) แต่เป็นเครื่องมือพิเศษสำหรับมากความต้องการเฉพาะ

ในแบบที่มันคือ "อันตราย" gotoในลักษณะเดียวกับที่ เป็นเครื่องมือที่มีประโยชน์ที่ไม่ควรพลาด แต่การใช้งานควรหายากในโค้ดของคุณ


ดังนั้นหากคุณไม่สามารถใช้คลาสพื้นฐานแบบโปร่งใสและการหล่อแบบไดนามิกได้คุณจะหลีกเลี่ยงอินเทอร์เฟซที่มีไขมันได้อย่างไร? คุณจะป้องกันไม่ให้ฟังก์ชั่นทุกอย่างที่คุณอาจต้องการเรียกใช้ประเภทใดประเภทหนึ่งไม่ให้เดือดไปถึงคลาสพื้นฐานได้อย่างไร

คำตอบขึ้นอยู่กับว่าคลาสพื้นฐานมีไว้เพื่ออะไร

คลาสพื้นฐานที่โปร่งใสเช่นnode_baseกำลังใช้เครื่องมือที่ไม่ถูกต้องสำหรับปัญหา รายการที่เชื่อมโยงได้รับการจัดการอย่างดีที่สุดโดยเทมเพลต ประเภทโหนดและการเชื่อมต่อจะถูกจัดเตรียมโดยประเภทเทมเพลต หากคุณต้องการใส่ประเภทโพลีมอร์ฟิกในรายการคุณสามารถทำได้ เพียงใช้BaseClass*เป็นTในการโต้เถียงแม่แบบ หรือตัวชี้อัจฉริยะที่คุณต้องการ

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

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

โซลูชันที่ทันสมัยสำหรับสิ่งนี้คือระบบลักษณะคอมโพเนนต์ Entityเป็นเพียงภาชนะของชุดส่วนประกอบโดยมีกาวบางส่วนกั้นไว้ ส่วนประกอบบางอย่างเป็นทางเลือก เอนทิตีที่ไม่มีการแสดงภาพไม่มีส่วนประกอบ "กราฟิก" เอนทิตีที่ไม่มี AI ไม่มีคอมโพเนนต์ "คอนโทรลเลอร์" และอื่น ๆ

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

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

นอกจากนี้ยังช่วยปฏิบัติตามหลักการความรับผิดชอบเดียว คลาสที่มีส่วนประกอบดังกล่าวมีหน้าที่เป็นผู้ถือส่วนประกอบเท่านั้น


จาก Matthew Walton:

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

เอาล่ะมาสำรวจกัน

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

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

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

ในภาษา C รูปแบบนี้ถูกสะกดvoid*และการใช้งานต้องใช้ความระมัดระวังเป็นอย่างมากเพื่อหลีกเลี่ยงการแตกหัก ใน C ++ รูปแบบนี้ถูกสะกดstd::experimental::any( จะสะกดเร็ว ๆ นี้std::any )

วิธีนี้ควรได้ผลคือ L จัดเตรียมnode_baseคลาสที่ใช้anyแทนข้อมูลจริงของคุณ เมื่อคุณได้รับข้อความรายการงานของคิวเธรดหรืออะไรก็ตามที่คุณกำลังทำอยู่คุณจะส่งข้อความนั้นanyไปยังประเภทที่เหมาะสมซึ่งทั้งผู้ส่งและผู้รับทราบ

ดังนั้นแทนที่จะสืบมาorange_nodeจากnode_dataคุณเพียงแค่ติดorangeภายในของnode_data's anyข้อมูลสมาชิก ผู้ใช้ปลายทางดึงข้อมูลและใช้any_castเพื่อแปลงเป็นไฟล์orange. orangeถ้าโยนล้มเหลวแล้วมันก็ไม่ได้

ตอนนี้ถ้าคุณคุ้นเคยกับการนำไปใช้anyแล้วคุณอาจจะพูดว่า "เดี๋ยวก่อนรอสักครู่: ใช้ RTTI any ภายในเพื่อสร้างany_castทำงาน" ซึ่งฉันตอบว่า "... ใช่"

นั่นคือจุดนั้นเป็นนามธรรม ลึกลงไปในรายละเอียดมีคนใช้ RTTI แต่ในระดับที่คุณควรปฏิบัติ RTTI โดยตรงไม่ใช่สิ่งที่คุณควรทำ

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

anyที่เรียกว่า มันใช้ RTTI แต่การใช้anyนั้นดีกว่าการใช้ RTTI โดยตรงเนื่องจากมันเหมาะกับความหมายที่ต้องการอย่างถูกต้องมากกว่า


10

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

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

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

ดังนั้นจะจัดการกับมันอย่างไร ... ขึ้นอยู่กับสถานการณ์ของคุณการมีคุณสมบัติเฉพาะโหนดใด ๆ ที่รวมอยู่ในอ็อบเจ็กต์แยกต่างหาก (เช่น API 'สีส้ม' ทั้งหมดอาจเป็นอ็อบเจ็กต์แยกต่างหาก) จากนั้นอ็อบเจ็กต์รูทสามารถมีฟังก์ชันเสมือนเพื่อส่งคืน API 'สีส้ม' โดยคืนค่า nullptr ตามค่าเริ่มต้นสำหรับอ็อบเจ็กต์ที่ไม่ใช่สีส้ม

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


6
เรื่องต้นทุนประสิทธิภาพ - ฉันวัด dynamic_cast <> เป็นต้นทุนประมาณ 2 ในแอปของเราบนโปรเซสเซอร์ 3GHz ซึ่งช้ากว่าการตรวจสอบ enum ประมาณ 1,000 เท่า (แอปของเรามีกำหนดเส้นตายหลักของลูป 11.1 มิลลิวินาทีดังนั้นเราจึงสนใจไมโครวินาทีเป็นอย่างมาก)
Crashworks

6
ประสิทธิภาพแตกต่างกันมากระหว่างการใช้งาน GCC ใช้การเปรียบเทียบตัวชี้ typeinfo ซึ่งรวดเร็ว MSVC ใช้การเปรียบเทียบสตริงซึ่งไม่เร็ว อย่างไรก็ตามวิธีการของ MSVC จะทำงานร่วมกับรหัสที่เชื่อมโยงกับไลบรารีเวอร์ชันต่างๆแบบคงที่หรือ DLL โดยที่วิธีการชี้ของ GCC เชื่อว่าคลาสในไลบรารีแบบคงที่แตกต่างจากคลาสในไลบรารีที่แชร์
Zan Lynx

1
@Crashworks เพื่อให้มีบันทึกที่สมบูรณ์ที่นี่: คอมไพเลอร์ (และเวอร์ชันใด) คืออะไร?
H. Guijt

@Crashworks สองการร้องขอข้อมูลที่คอมไพเลอร์สร้างผลลัพธ์ที่คุณสังเกต; ขอบคุณ.
underscore_d

@underscore_d: MSVC.
Crashworks

9

C ++ สร้างขึ้นจากแนวคิดของการตรวจสอบประเภทคงที่

[1] RTTI นั่นคือdynamic_castและtype_idคือการตรวจสอบประเภทไดนามิก

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


เรื่อง

" ฉันไม่รู้วิธีที่สะอาดในการใช้เทมเพลตหรือวิธีการอื่น ๆ

คุณสามารถทำกระบวนการที่แตกต่างกันของโหนดของกราฟด้วยการตรวจสอบประเภทคงที่และไม่มีการหล่อใด ๆ ผ่านรูปแบบผู้เยี่ยมชมเช่นเช่นนี้:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

สิ่งนี้ถือว่าชุดของประเภทโหนดเป็นที่รู้จัก

เมื่อไม่เป็นเช่นนั้นรูปแบบผู้เยี่ยมชม (มีหลายรูปแบบ) สามารถแสดงได้ด้วยการร่ายแบบรวมศูนย์สองสามแบบหรือเพียงรูปแบบเดียว


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


[1] Run Time ข้อมูลประเภท


1
"สิ่งที่น่าตลก" อย่างหนึ่งที่ควรทราบก็คือเราสามารถลดจำนวนรหัส (การเปลี่ยนแปลงเมื่อเพิ่มประเภท) ที่จำเป็นสำหรับรูปแบบผู้เยี่ยมชมคือการใช้ RTTI เพื่อ "ไต่ระดับ" ลำดับชั้น ฉันรู้จักสิ่งนี้ในชื่อ "รูปแบบผู้เยี่ยมชมแบบ acyclic"
Daniel Jour

3

แน่นอนว่ามีสถานการณ์ที่ความหลากหลายไม่สามารถช่วยได้: ชื่อ typeidช่วยให้คุณเข้าถึงชื่อของประเภทแม้ว่าวิธีการเข้ารหัสชื่อนี้จะถูกกำหนดโดยการนำไปใช้งาน แต่โดยปกติแล้วนี่ไม่ใช่ปัญหาเนื่องจากคุณสามารถเปรียบเทียบสองtypeid-s:

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

เช่นเดียวกันกับแฮช

[... ] RTTI "ถือว่าเป็นอันตราย"

เป็นอันตรายอย่างแน่นอนคือการพูดเกินจริง: RTTI มีข้อเสียอยู่บ้าง แต่ก็เป็นเช่นนั้นมีข้อได้เปรียบมากเกินไป

คุณไม่จำเป็นต้องใช้ RTTI อย่างแท้จริง RTTI เป็นเครื่องมือในการแก้ปัญหา OOP: หากคุณใช้กระบวนทัศน์อื่นสิ่งเหล่านี้น่าจะหายไป C ไม่มี RTTI แต่ยังใช้งานได้ C ++ สนับสนุน OOP อย่างสมบูรณ์แทนและให้เครื่องมือหลายอย่างเพื่อเอาชนะปัญหาบางอย่างที่อาจต้องใช้ข้อมูลรันไทม์: หนึ่งในนั้นคือ RTTI ซึ่งแม้ว่าจะมาพร้อมกับราคาก็ตาม หากคุณไม่สามารถจ่ายได้สิ่งที่คุณควรระบุหลังจากการวิเคราะห์ประสิทธิภาพที่ปลอดภัยแล้วยังมีโรงเรียนเก่าอยู่void*: ฟรี ไม่มีค่าใช้จ่าย แต่คุณไม่มีความปลอดภัย ดังนั้นทุกอย่างเกี่ยวกับการเทรด


  • คอมไพเลอร์บางตัวไม่ใช้ / RTTI ไม่ได้เปิดใช้งานเสมอ
    ฉันไม่ได้ซื้ออาร์กิวเมนต์นี้จริงๆ เหมือนกับการบอกว่าฉันไม่ควรใช้ฟีเจอร์ C ++ 14 เพราะมีคอมไพเลอร์ที่ไม่รองรับ แต่ก็ไม่มีใครกีดกันฉันจากการใช้คุณสมบัติ C ++ 14

หากคุณเขียน (อาจเป็นไปอย่างเคร่งครัด) ตามโค้ด C ++ คุณสามารถคาดหวังพฤติกรรมเดียวกันได้โดยไม่คำนึงถึงการนำไปใช้ การใช้งานที่เป็นไปตามมาตรฐานจะต้องรองรับคุณสมบัติ C ++ มาตรฐาน

แต่โปรดพิจารณาว่าในบางสภาพแวดล้อมที่ C ++ กำหนด («อิสระ») ไม่จำเป็นต้องจัดเตรียม RTTI และไม่ทำข้อยกเว้นvirtualและอื่น ๆ RTTI ต้องการเลเยอร์พื้นฐานเพื่อให้ทำงานได้อย่างถูกต้องซึ่งเกี่ยวข้องกับรายละเอียดระดับต่ำเช่น ABI และข้อมูลประเภทจริง


ฉันเห็นด้วยกับ Yakk เกี่ยวกับ RTTI ในกรณีนี้ ใช่มันสามารถใช้ได้ แต่มันถูกต้องตามหลักเหตุผลหรือไม่? การที่ภาษาอนุญาตให้คุณข้ามเช็คนี้ไม่ได้หมายความว่าควรทำ

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