วัตถุประสงค์ของคำหลัก“ สุดท้าย” ใน C ++ 11 สำหรับฟังก์ชั่นคืออะไร?


143

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


30
" ไม่เพียงพอที่จะประกาศว่าไม่ใช่ฟังก์ชัน" ขั้นสุดท้าย "ของคุณ" ไม่ใช่ "เสมือนฟังก์ชั่นแทนที่จะเป็นเสมือนโดยนัยไม่ว่าคุณจะใช้virtualคำหลักหรือไม่ก็ตาม
ildjarn

13
@ildjarn ว่าไม่เป็นความจริงถ้าพวกเขาไม่ได้ถูกประกาศให้เป็นเสมือนในชั้นซุปเปอร์ที่คุณไม่สามารถได้รับมาจากชั้นเรียนและเปลี่ยนวิธีการที่ไม่เสมือนเป็นเสมือนหนึ่ง ..
แดน O

10
@DanO ฉันคิดว่าคุณไม่สามารถแทนที่ แต่คุณสามารถ "ซ่อน" วิธีการที่ .. ซึ่งนำไปสู่ปัญหามากมายเนื่องจากคนไม่ได้หมายถึงการซ่อนวิธีการ
Alex Kremer

16
@DanO: ถ้ามันไม่ใช่เวอร์ชวลในซูเปอร์คลาสมันจะไม่ "เอาชนะ"
ildjarn

2
อีกครั้ง "การเอาชนะ " มีความหมายเฉพาะที่นี่ซึ่งจะให้พฤติกรรม polymorphic กับฟังก์ชั่นเสมือนจริง ในตัวอย่างของคุณfuncไม่ได้เป็นเสมือนดังนั้นมีอะไรที่จะแทนที่จึงไม่มีอะไรที่จะทำเครื่องหมายว่าหรือoverride final
ildjarn

คำตอบ:


129

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

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};

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

8
@lezebulon: คลาสใบไม้ของคุณไม่จำเป็นต้องทำเครื่องหมายฟังก์ชั่นเสมือนถ้าคลาสซุปเปอร์ประกาศว่าเป็นเสมือน
Dan O

5
วิธีการในชั้นเรียนใบไม้เป็นเสมือนโดยปริยายหากพวกเขาเป็นเสมือนในชั้นฐาน ฉันคิดว่าคอมไพเลอร์ควรเตือนถ้าไม่มี 'เสมือน' โดยนัยนี้
Aaron McDaid

@AaronMcDaid: คอมไพเลอร์มักจะเตือนเกี่ยวกับรหัสที่ถูกต้องอาจทำให้เกิดความสับสนหรือข้อผิดพลาด ฉันไม่เคยเห็นใครที่ประหลาดใจกับคุณลักษณะเฉพาะของภาษาในลักษณะที่อาจทำให้เกิดปัญหาใด ๆ ดังนั้นฉันจึงไม่รู้ว่าข้อผิดพลาดที่เป็นประโยชน์อาจเกิดขึ้นได้อย่างไร ในทางตรงกันข้ามการลืมvirtualสาเหตุอาจทำให้เกิดข้อผิดพลาดและ C ++ 11 เพิ่มoverrideแท็กลงในฟังก์ชันที่จะตรวจจับสถานการณ์นั้นและไม่สามารถคอมไพล์เมื่อฟังก์ชันที่ตั้งใจจะแทนที่จริง ๆซ่อน
David Rodríguez - dribeas

1
จากบันทึกการเปลี่ยนแปลง GCC 4.9: "โมดูลการวิเคราะห์การสืบทอดประเภทใหม่ปรับปรุง devirtualization ตอนนี้ Devirtualization คำนึงถึงชื่อพื้นที่ว่างที่ไม่ระบุชื่อและคำหลักสุดท้ายของ C ++ 11" - ดังนั้นจึงไม่ใช่แค่น้ำตาลเชิงไวยากรณ์
kfsone

127
  • เป็นการป้องกันคลาสจากการสืบทอด จากWikipedia :

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

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • มันถูกใช้เพื่อทำเครื่องหมายฟังก์ชันเสมือนเพื่อป้องกันไม่ให้ถูกเขียนทับในคลาสที่ได้รับ:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };

วิกิพีเดียยังทำให้ประเด็นที่น่าสนใจอีก :

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

หมายความว่าอนุญาตสิ่งต่อไปนี้:

int const final = 0;     // ok
int const override = 1;  // ok

1
ขอบคุณ แต่ฉันลืมที่จะพูดถึงว่าคำถามของฉันกังวลการใช้งานของ "สุดท้าย" ด้วยวิธีการ
lezebulon

คุณพูดถึง @lezebulon :-) "อะไรคือจุดประสงค์ของคำหลัก" สุดท้าย "ใน C ++ 11 สำหรับฟังก์ชั่น " (ความสำคัญของฉัน)
แอรอน McDaid

คุณแก้ไขหรือไม่ ฉันไม่เห็นข้อความใด ๆ ที่ระบุว่า "แก้ไข x นาทีที่ผ่านมาโดย lezebulon" มันเกิดขึ้นได้อย่างไร? คุณอาจแก้ไขได้อย่างรวดเร็วหลังจากส่งหรือไม่
Aaron McDaid

5
@Aaron: การแก้ไขที่ทำภายในห้านาทีหลังจากโพสต์ยังไม่ปรากฏในประวัติการแก้ไข
ildjarn

@Nawaz: ทำไมพวกเขาไม่ได้เป็นเพียงแค่คำหลัก specifiers? เป็นเพราะเหตุผลด้านความเข้ากันได้หมายความว่าเป็นไปได้หรือไม่ว่าโค้ดที่มีอยู่ก่อนหน้า C ++ 11 จะใช้ขั้นตอนสุดท้าย & แทนที่สำหรับวัตถุประสงค์อื่นหรือไม่?
ทำลายล้าง

45

"ขั้นสุดท้าย" ยังช่วยให้การเพิ่มประสิทธิภาพคอมไพเลอร์เพื่อหลีกเลี่ยงการโทรทางอ้อม:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

ด้วย "final" คอมไพเลอร์สามารถโทรCDerived::DoSomething()โดยตรงจากภายในBlah()หรือแม้แต่อินไลน์ ไม่ว่าจะมีการสร้างทางอ้อมโทรภายในBlah()เพราะอาจเรียกได้ว่าอยู่ในชั้นเรียนมาแทนที่ซึ่งมีBlah()DoSomething()


29

ไม่มีอะไรที่จะเพิ่มในด้านความหมายของ "ขั้นสุดท้าย"

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

ข้อเสียอย่างหนึ่งที่สำคัญของ vtables คือสำหรับวัตถุเสมือนใด ๆ (สมมติว่า 64- บิตบน Intel CPU ทั่วไป) ตัวชี้เพียงอย่างเดียวกินได้ถึง 25% (8 จาก 64 ไบต์) ของสายแคช ในแอพพลิเคชั่นที่ฉันชอบเขียนมันเจ็บมาก (และจากประสบการณ์ของฉันมันเป็นอาร์กิวเมนต์ # 1 กับ C ++ จากมุมมองการทำงานที่พิถีพิถันคือโดยโปรแกรมเมอร์ C)

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

เทคนิคนี้เป็นที่รู้จักกันDevirtualization คำที่ควรจดจำ :-)

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

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly


23
8 คือ 25% ของ 64?
ildjarn

6
ใครรู้ว่าคอมไพเลอร์ที่ใช้ประโยชน์จากตอนนี้หรือไม่
Vincent Fourmond

สิ่งเดียวกันที่ฉันต้องการจะพูด
crazii

8

สุดท้ายไม่สามารถนำไปใช้กับฟังก์ชั่นที่ไม่ใช่เสมือน

error: only virtual member functions can be marked 'final'

มันไม่มีความหมายมากนักที่จะสามารถทำเครื่องหมายวิธีการที่ไม่ใช่เสมือนเป็น 'ขั้นสุดท้าย' ป.ร. ให้ไว้

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()A::fooมักจะเรียก

แต่ถ้า A :: foo เป็นvirtualเช่นนั้น B :: foo จะลบล้างมัน สิ่งนี้อาจไม่เป็นที่พึงปรารถนาและด้วยเหตุนี้จึงสมเหตุสมผลที่จะทำให้ฟังก์ชันเสมือนจริงเป็นที่สิ้นสุด

คำถามคือทำไมอนุญาตขั้นสุดท้ายในฟังก์ชั่นเสมือน หากคุณมีลำดับชั้นลึก:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

จากนั้นfinalจะวาง 'พื้น' ลงบนจำนวนที่สามารถทำได้ คลาสอื่นสามารถขยาย A และ B และแทนที่ได้fooแต่คลาสนั้นขยาย C ดังนั้นจึงไม่อนุญาต

ดังนั้นจึงอาจไม่สมเหตุสมผลที่จะทำให้ฟู 'ระดับบนสุด' finalแต่อาจทำให้รู้สึกลดลง

(ฉันคิดว่ามีช่องว่างที่จะขยายคำสุดท้ายและแทนที่สมาชิกที่ไม่ใช่เสมือนพวกเขาอาจมีความหมายที่แตกต่างกันไป)


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

@lezebulon ฉันแก้ไขคำถามของฉัน แต่ฉันก็สังเกตเห็นคำตอบของ DanO - มันเป็นคำตอบที่ชัดเจนในสิ่งที่ฉันพยายามจะพูด
Aaron McDaid

ฉันไม่ได้เป็นผู้เชี่ยวชาญ finalแต่ฉันรู้สึกว่าบางครั้งมันอาจจะทำให้ความรู้สึกที่จะทำให้ฟังก์ชั่นระดับบนสุด ตัวอย่างเช่นถ้าคุณรู้ว่าคุณต้องการทั้งหมดShapeเพื่อfoo()บางสิ่งบางอย่างที่กำหนดไว้ล่วงหน้าและแน่นอนว่าไม่มีรูปร่างที่ได้มาควรแก้ไข หรือฉันผิดและมีรูปแบบการจ้างงานที่ดีกว่าสำหรับกรณีนี้หรือไม่ แก้ไข: โอ้อาจเป็นเพราะในกรณีนั้นเราไม่ควรfoo() virtualเริ่มต้นระดับบนสุดด้วย? แต่ก็ยังสามารถซ่อนได้แม้ว่าจะเรียกอย่างถูกต้อง (polymorphically) ผ่านShape*...
Andrew Cheong

8

กรณีการใช้งานสำหรับคำหลัก 'สุดท้าย' ที่ฉันชื่นชอบมีดังนี้:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};

1
ใช่นี่เป็นตัวอย่างของรูปแบบวิธีการของแม่แบบ และ pre-C ++ 11 มันเป็น TMP เสมอที่ฉันคิดว่า C ++ มีคุณสมบัติภาษาเช่น "final" อย่าง Java
Kaitain

6

final เพิ่มเจตนาชัดเจนที่จะไม่แทนที่ฟังก์ชันของคุณและจะทำให้เกิดข้อผิดพลาดของคอมไพเลอร์หากมีการละเมิด:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

ในฐานะที่เป็นรหัสย่อมาก็รวบรวมและแทนที่B::foo เป็นเสมือนจริงโดยวิธี อย่างไรก็ตามหากเราเปลี่ยน # 1 เป็นนี่เป็นข้อผิดพลาดของคอมไพเลอร์และเราไม่ได้รับอนุญาตให้แทนที่ใด ๆ เพิ่มเติมในชั้นเรียนที่ได้รับA::fooB::foovirtual int foo() finalA::foo

โปรดทราบว่าสิ่งนี้ไม่อนุญาตให้เรา "เปิดใหม่" ลำดับชั้นใหม่นั่นคือไม่มีวิธีที่จะสร้างB::fooฟังก์ชั่นใหม่ที่ไม่เกี่ยวข้องซึ่งสามารถเป็นอิสระที่หัวของลำดับชั้นเสมือนใหม่ เมื่อฟังก์ชั่นเป็นที่สิ้นสุดก็ไม่สามารถประกาศอีกครั้งในชั้นเรียนที่ได้รับใด ๆ


5

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

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

ตามที่ผู้โพสต์คนอื่นชี้ให้เห็นมันไม่สามารถนำไปใช้กับฟังก์ชั่นที่ไม่ใช่เสมือน

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


1

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

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

นอกจากนี้:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}

0

เสริมคำตอบของ Mario Knezović:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

รหัสข้างต้นแสดงให้เห็นถึงทฤษฎี แต่ไม่ได้ทดสอบจริงกับคอมไพเลอร์จริง นิยมมากถ้าใครวางเอาท์พุทถอดชิ้นส่วน

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