Apa tujuan dari kata kunci "final" di C ++ 11 untuk fungsi?

143

Apa tujuan finalkata kunci dalam fungsi C ++ 11? Saya memahaminya mencegah overriding fungsi oleh kelas turunan, tetapi jika ini masalahnya, bukankah itu cukup untuk menyatakan sebagai non-virtual finalfungsi Anda ? Apakah ada hal lain yang saya lewatkan di sini?

lezebulon
sumber
30
" Tidakkah cukup untuk menyatakan sebagai fungsi" final "non-virtual Anda? Tidak, fungsi-fungsi utama secara virtual virtual apakah Anda menggunakan virtualkata kunci atau tidak.
ildjarn
13
@ildjarn itu tidak benar jika mereka tidak dinyatakan sebagai virtual di kelas super, Anda tidak dapat berasal dari kelas dan mengubah metode non-virtual menjadi virtual ..
Dan O
10
@DanO saya pikir Anda tidak dapat menimpa tetapi Anda dapat "menyembunyikan" metode seperti itu .. yang mengarah ke banyak masalah karena orang tidak bermaksud menyembunyikan metode.
Alex Kremer
16
@DanO: Jika ini bukan virtual di kelas super maka itu tidak akan "menimpa".
ildjarn
2
Sekali lagi, " pengesampingan " memiliki arti khusus di sini, yaitu untuk memberikan perilaku polimorfik ke fungsi virtual. Dalam contoh Anda funcbukan virtual, jadi tidak ada yang menimpa dan dengan demikian tidak ada yang menandai overrideatau final.
ildjarn

Jawaban:

129

Apa yang Anda hilang, sebagai idljarn telah disebutkan dalam komentar adalah bahwa jika Anda meng-override fungsi dari kelas dasar, maka Anda tidak mungkin menandainya sebagai non-virtual:

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!
};
David Rodríguez - dribeas
sumber
Terima kasih! ini adalah poin yang saya lewatkan: yaitu bahwa bahkan kelas "daun" Anda perlu menandai fungsinya sebagai virtual bahkan jika mereka bermaksud untuk mengesampingkan fungsi, dan tidak untuk ditimpa sendiri
lezebulon
8
@lezebulon: Kelas daun Anda tidak perlu menandai fungsi sebagai virtual jika kelas super menyatakannya sebagai virtual.
Dan O
5
Metode dalam kelas daun secara implisit virtual jika mereka virtual di kelas dasar. Saya pikir kompiler harus memperingatkan jika 'virtual' implisit ini hilang.
Aaron McDaid
@AaronMcDaid: Compiler biasanya memperingatkan tentang kode yang, jika benar, dapat menyebabkan kebingungan atau kesalahan. Saya belum pernah melihat orang yang terkejut dengan fitur khusus bahasa ini dengan cara yang dapat menyebabkan masalah, jadi saya tidak benar-benar tahu seberapa berguna kesalahan itu. Sebaliknya, melupakan virtualdapat menyebabkan kesalahan, dan C ++ 11 menambahkan overridetag ke fungsi yang akan mendeteksi situasi itu dan gagal mengkompilasi ketika fungsi yang dimaksudkan untuk menimpa sebenarnya menyembunyikan
David Rodríguez - dribeas
1
Dari GCC 4.9 catatan perubahan: "Modul analisis pewarisan tipe baru meningkatkan devirtualization. Devirtualization sekarang memperhitungkan ruang-nama anonim dan kata kunci akhir C ++ 11" - jadi itu bukan hanya gula sintaksis, tetapi juga memiliki potensi manfaat optimasi.
kfsone
126
  • Ini untuk mencegah kelas dari diwarisi. Dari Wikipedia :

    C ++ 11 juga menambahkan kemampuan untuk mencegah pewarisan dari kelas atau hanya mencegah metode utama dalam kelas turunan. Ini dilakukan dengan final pengidentifikasi khusus. Sebagai contoh:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • Ini juga digunakan untuk menandai fungsi virtual untuk mencegahnya ditimpa dalam kelas turunan:

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

Wikipedia lebih lanjut membuat poin menarik :

Perhatikan bahwa kata kunci bahasa overridejuga finaltidak. Mereka adalah pengidentifikasi teknis; mereka hanya mendapatkan makna khusus ketika digunakan dalam konteks spesifik itu . Di lokasi lain mana pun, mereka bisa menjadi pengidentifikasi yang valid.

Itu berarti, yang berikut ini diperbolehkan:

int const final = 0;     // ok
int const override = 1;  // ok
Nawaz
sumber
1
terima kasih, tapi saya lupa menyebutkan bahwa pertanyaan saya berkaitan dengan penggunaan "final" dengan metode
lezebulon
Anda memang menyebutkannya @lezebulon :-) "apa tujuan dari kata kunci" final "di C ++ 11 untuk fungsi ". (Penekanan saya)
Aaron McDaid
Anda mengeditnya? Saya tidak melihat pesan apa pun yang mengatakan "diedit x menit yang lalu oleh lezebulon". Bagaimana itu bisa terjadi? Mungkin Anda mengeditnya dengan sangat cepat setelah mengirimkannya?
Aaron McDaid
5
@ Harun: Suntingan yang dilakukan dalam lima menit setelah posting tidak tercermin dalam riwayat revisi.
ildjarn
@Nawaz: mengapa mereka bukan kata kunci hanya penentu? Apakah karena alasan kompatibilitas berarti ada kemungkinan bahwa kode yang sudah ada sebelum C ++ 11 menggunakan final & override untuk keperluan lain?
Destructor
45

"final" juga memungkinkan pengoptimalan kompiler untuk mem-bypass panggilan tidak langsung:

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

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

  void Blah( void ) { DoSomething(); }

};

dengan "final", kompiler dapat memanggil CDerived::DoSomething()langsung dari dalam Blah(), atau bahkan inline. Tanpa itu, ia harus menghasilkan panggilan tidak langsung di dalam Blah()karena Blah()dapat disebut di dalam kelas turunan yang telah ditimpa DoSomething().

hijau chris
sumber
29

Tidak ada yang ditambahkan ke aspek semantik "final".

Tapi saya ingin menambahkan komentar chris green bahwa "final" mungkin menjadi teknik optimisasi kompiler yang sangat penting dalam waktu yang tidak begitu lama. Tidak hanya dalam kasus sederhana yang ia sebutkan, tetapi juga untuk hierarki kelas dunia nyata yang lebih kompleks yang dapat "ditutup" oleh "final", sehingga memungkinkan kompiler untuk menghasilkan kode pengiriman yang lebih efisien daripada dengan pendekatan vtable yang biasa.

Salah satu kelemahan utama dari vtables adalah bahwa untuk objek virtual semacam itu (dengan asumsi 64-bit pada CPU Intel khas) pointer saja memakan 25% (8 dari 64 byte) dari garis cache. Dalam jenis aplikasi yang saya sukai untuk menulis, ini sangat menyakitkan. (Dan dari pengalaman saya itu adalah argumen # 1 terhadap C ++ dari sudut pandang kinerja purist, yaitu oleh programmer C.)

Dalam aplikasi yang membutuhkan kinerja ekstrem, yang tidak biasa untuk C ++, ini mungkin memang menjadi luar biasa, tidak perlu untuk menyelesaikan masalah ini secara manual dalam gaya C atau juggling Template aneh.

Teknik ini dikenal sebagai Devirtualization . Suatu istilah yang patut diingat. :-)

Ada sebuah pidato hebat baru-baru ini oleh Andrei Alexandrescu yang menjelaskan bagaimana Anda dapat mengatasi situasi seperti itu hari ini dan bagaimana "final" mungkin menjadi bagian dari penyelesaian kasus serupa "secara otomatis" di masa depan (dibahas dengan pendengar):

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

Mario Knezović
sumber
23
8 adalah 25% dari 64?
ildjarn
6
Adakah yang tahu kompiler yang memanfaatkannya sekarang?
Vincent Fourmond
hal yang sama yang ingin saya katakan.
crazii
8

Final tidak dapat diterapkan pada fungsi non-virtual.

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

Itu tidak akan sangat bermakna untuk dapat menandai metode non-virtual sebagai 'final'. Diberikan

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()akan selalu menelepon A::foo.

Tetapi, jika A :: foo adalah virtual, maka B :: foo akan menimpanya. Ini mungkin tidak diinginkan, dan karenanya masuk akal untuk membuat fungsi virtual menjadi final.

Namun pertanyaannya adalah, mengapa memungkinkan final pada fungsi virtual. Jika Anda memiliki hierarki yang dalam:

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 */ };

Kemudian finalmeletakkan 'lantai' pada seberapa banyak overriding dapat dilakukan. Kelas-kelas lain dapat memperluas A dan B dan menimpa mereka foo, tetapi suatu kelas memperpanjang C maka itu tidak diizinkan.

Jadi mungkin tidak masuk akal untuk membuat foo 'top-level' final, tetapi mungkin masuk akal lebih rendah.

(Saya pikir, ada ruang untuk memperluas kata-kata final dan menimpa ke anggota non-virtual. Mereka akan memiliki arti yang berbeda.)

Aaron McDaid
sumber
terima kasih untuk contohnya, itu adalah sesuatu yang saya tidak yakin tentang. Tapi tetap saja: apa gunanya memiliki fungsi final (dan virtual)? Pada dasarnya Anda tidak akan pernah dapat menggunakan fakta bahwa fungsi ini virtual karena tidak dapat diganti
lezebulon
@lezebulon, saya mengedit pertanyaan saya. Tapi kemudian saya perhatikan jawaban DanO - itu jawaban yang jelas dan bagus dari apa yang saya coba katakan.
Aaron McDaid
Saya bukan seorang ahli, tetapi saya merasa bahwa kadang-kadang mungkin masuk akal untuk membuat fungsi tingkat atas final. Misalnya, jika Anda tahu Anda ingin semua Shape- foo()sesuatu yang sudah ditentukan sebelumnya dan pasti bahwa tidak ada bentuk turunan yang harus dimodifikasi. Atau, apakah saya salah dan ada pola yang lebih baik untuk digunakan dalam kasus itu? EDIT: Oh, mungkin karena dalam hal itu, seseorang seharusnya tidak membuat tingkat atas foo() virtualuntuk memulai? Tapi tetap saja, itu bisa disembunyikan, bahkan jika dipanggil dengan benar (secara polimorfik) melalui Shape*...
Andrew Cheong
8

Kasus penggunaan untuk kata kunci 'final' yang saya sukai adalah sebagai berikut:

// 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 */}
};
John muda
sumber
1
Ya, ini pada dasarnya adalah contoh Pola Metode Templat. Dan pra-C ++ 11, selalu TMP yang membuat saya berharap bahwa C ++ memiliki fitur bahasa seperti "final", seperti Java.
Kaitain
6

final menambahkan maksud eksplisit agar fungsi Anda tidak ditimpa, dan akan menyebabkan kesalahan kompilator jika ini dilanggar:

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

Sebagai kode berdiri, itu mengkompilasi, dan B::foomenimpa A::foo. B::fooomong-omong juga virtual. Namun, jika kita mengubah # 1 menjadi virtual int foo() final, maka ini adalah kesalahan penyusun, dan kami tidak diizinkan untuk menimpanyaA::foo lebih jauh di kelas turunan.

Perhatikan bahwa ini tidak memungkinkan kita untuk "membuka kembali" hierarki baru, yaitu tidak ada cara untuk membuat B::foofungsi baru yang tidak terkait yang dapat secara independen menjadi kepala hirarki virtual baru. Setelah suatu fungsi bersifat final, ia tidak akan pernah dapat dideklarasikan lagi di kelas turunan mana pun.

Kerrek SB
sumber
5

Kata kunci terakhir memungkinkan Anda untuk mendeklarasikan metode virtual, menimpanya sebanyak N kali, dan kemudian mengamanatkan bahwa 'ini tidak lagi dapat ditimpa'. Ini akan berguna dalam membatasi penggunaan kelas turunan Anda, sehingga Anda dapat mengatakan "Saya tahu kelas super saya memungkinkan Anda menimpa ini, tetapi jika Anda ingin berasal dari saya, Anda tidak bisa!".

struct Foo
{
   virtual void DoStuff();
}

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

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

Seperti yang ditunjukkan poster lainnya, itu tidak dapat diterapkan pada fungsi non-virtual.

Salah satu tujuan dari kata kunci terakhir adalah untuk mencegah pengesampingan metode yang tidak disengaja. Dalam contoh saya, DoStuff () mungkin merupakan fungsi pembantu yang hanya perlu diubah oleh kelas turunan untuk mendapatkan perilaku yang benar. Tanpa akhir, kesalahan tidak akan ditemukan sampai pengujian.

Dan O
sumber
1

Kata kunci terakhir dalam C ++ ketika ditambahkan ke suatu fungsi, mencegahnya ditimpa oleh kelas dasar. Juga ketika ditambahkan ke kelas mencegah pewarisan jenis apa pun. Pertimbangkan contoh berikut yang menunjukkan penggunaan specifier akhir. Program ini gagal dalam kompilasi.

#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;
}

Juga:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}
Krishna Ganeriwal
sumber
0

Tambahan untuk jawaban 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();

Kode di atas menunjukkan teorinya, tetapi tidak benar-benar diuji pada kompiler nyata. Sangat dihargai jika ada yang menempel output yang dibongkar.

crazii
sumber