ใน C ++ ฉันควรใช้ขั้นสุดท้ายในการประกาศวิธีเสมือนเมื่อใด


11

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

คำถามของฉันคือต่อไป:

มีกรณีใดที่มีประโยชน์เมื่อฉันควรใช้finalในvirtualการประกาศวิธี?


ด้วยเหตุผลเดียวกันทั้งหมดที่วิธีการทำ Java เป็นที่สิ้นสุด?
58

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

เดาว่าฉันไม่เก่งในการอธิบายเรื่องต่าง ๆ - ฉันพยายามพูดแบบเดียวกับ Mike Nakis
58

คำตอบ:


12

สรุปของสิ่งที่finalคำหลักไม่:สมมติว่าเรามีฐานชั้นและชั้นเรียนมาA Bฟังก์ชั่นf()อาจมีการประกาศในAฐานะvirtualซึ่งหมายความว่าชั้นBอาจแทนที่มัน แต่คลาสนั้นBอาจต้องการให้คลาสที่ได้รับมาจากต่อไปBนั้นไม่สามารถแทนที่f()ได้ นั่นคือเมื่อเราจำเป็นต้องประกาศf()เป็นในfinal Bหากไม่มีfinalคำหลักเมื่อเรากำหนดวิธีการเป็นเสมือนคลาสใด ๆ ที่ได้รับจะมีอิสระที่จะแทนที่ได้ finalคำหลักที่จะใช้ในการหมดสิ้นไปเสรีภาพนี้

ตัวอย่างของทำไมคุณจะต้องเป็นสิ่งที่ดังกล่าวระดับสมมติว่าAกำหนดฟังก์ชันเสมือนprepareEnvelope()และระดับBการแทนที่มันและการดำเนินการเป็นลำดับของการโทรไปยังวิธีเสมือนของตัวเองstuffEnvelope(), และlickEnvelope() sealEnvelope()คลาสBตั้งใจที่จะอนุญาตให้คลาสที่ได้รับมาเพื่อแทนที่วิธีเสมือนเหล่านี้เพื่อให้มีการใช้งานของตัวเอง แต่คลาสBไม่ต้องการให้คลาสที่ได้รับมาแทนที่prepareEnvelope()ดังนั้นจึงเปลี่ยนลำดับของสิ่งต่างๆเลียประทับตราหรือละเว้นการเรียกใช้หนึ่งในนั้น ดังนั้นในกรณีนี้คลาสจะBประกาศว่าprepareEnvelope()เป็นที่สุด


ก่อนอื่นขอบคุณสำหรับคำตอบ แต่นั่นไม่ใช่สิ่งที่ฉันเขียนในประโยคแรกของคำถาม สิ่งที่ฉันต้องการจริงๆเป็นกรณีเมื่อclass B might wish that any class which is further derived from B should not be able to override f()ใด มีกรณีในโลกแห่งความเป็นจริงที่คลาส B และเมธอด f () มีอยู่จริงและเหมาะสมหรือไม่?
metamaker

ใช่ฉันขอโทษแขวนบนฉันเขียนตัวอย่างตอนนี้
Mike Nakis

@metamaker ที่นั่นคุณไป
Mike Nakis

1
ตัวอย่างที่ดีจริงๆนี่คือคำตอบที่ดีที่สุดจนถึงตอนนี้ ขอบคุณ!
metamaker

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

2

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

ตัวอย่าง

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

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

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

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

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

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

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

นั่นคือสิ่งที่finalมีประโยชน์จากมุมมองการออกแบบ / สถาปัตยกรรม

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

คำถาม

จากความคิดเห็นที่:

ทำไมจะถึงขั้นสุดท้ายและเสมือนเคยใช้ในเวลาเดียวกัน

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

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

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

และบางคนอาจชอบโวหารเพื่อระบุอย่างชัดเจนว่าBar::fเป็นเสมือนจริงเช่น:

struct Bar: Foo
{
   virtual void f() final {...}
};

สำหรับฉันแล้วมันเป็นเรื่องซ้ำซ้อนที่จะใช้ทั้งสองvirtualและตัวfinalระบุสำหรับฟังก์ชั่นเดียวกันในบริบทนี้ (เช่นเดียวกันvirtualกับoverride) แต่มันเป็นเรื่องของสไตล์ในกรณีนี้ บางคนอาจพบว่าvirtualสื่อสารสิ่งที่มีคุณค่าที่นี่เช่นเดียวexternกับการประกาศฟังก์ชั่นด้วยการเชื่อมโยงภายนอก (แม้ว่าจะเป็นตัวเลือกที่ไม่มีตัวระบุลิงก์อื่น ๆ )


คุณวางแผนที่จะแทนที่privateวิธีการภายในVehicleชั้นเรียนของคุณได้อย่างไร คุณไม่ได้หมายความว่าprotectedแทนหรือ?
Andy

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

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

ฉันคิดว่าการตั้งค่าเป็นแบบส่วนตัวจะไม่ได้รวบรวม ตัวอย่างเช่น Java หรือ C # ไม่อนุญาตสิ่งนั้น C ++ ทำให้ฉันประหลาดใจทุกวัน แม้หลังจาก 10 ปีของการเขียนโปรแกรม
Andy

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

0
  1. มันช่วยเพิ่มประสิทธิภาพมากเพราะมันอาจจะเป็นที่รู้จักกันในเวลารวบรวมฟังก์ชั่นที่เรียกว่า

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


ทำไมfinalและvirtualจะถูกนำมาใช้ในเวลาเดียวกัน?
Robert Harvey

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

@RobertHarvey ABI เสถียรภาพ ในกรณีอื่น ๆ ก็อาจจะและfinal override
Deduplicator

@metamaker ฉันมีคำถามเดียวกันที่กล่าวถึงในความคิดเห็นที่นี่ - programmers.stackexchange.com/questions/256778/ … - แล้วก็สนุกไปกับการสนทนาอื่น ๆ อีกมากมายตั้งแต่ถามเท่านั้น! โดยพื้นฐานแล้วถ้าคอมไพเลอร์ / ตัวเชื่อมโยงที่ปรับให้เหมาะสมสามารถกำหนดได้ว่าการอ้างอิง / ตัวชี้อาจแทนคลาสที่ได้รับ & ดังนั้นจึงจำเป็นต้องมีการโทรเสมือน หากวิธีการ (หรือคลาส) พิสูจน์ได้ว่าไม่สามารถถูกแทนที่ได้นอกเหนือจากประเภทคงที่ของการอ้างอิงพวกเขาสามารถ devirtualise: โทรโดยตรง หากมีข้อสงสัยใด ๆ - บ่อยครั้ง - พวกเขาจะต้องโทรหาจริง การประกาศfinalสามารถช่วยพวกเขาได้จริง ๆ
อันเดอร์คอร์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.