C ++ kata kunci "virtual" untuk fungsi di kelas turunan. Apakah itu perlu?

221

Dengan definisi struct yang diberikan di bawah ini ...

struct A {
    virtual void hello() = 0;
};

Pendekatan # 1:

struct B : public A {
    virtual void hello() { ... }
};

Pendekatan # 2:

struct B : public A {
    void hello() { ... }
};

Apakah ada perbedaan antara kedua cara ini untuk mengesampingkan fungsi halo?

Anarki
sumber
65
Di C ++ 11 Anda dapat menulis "void hello () override {}" untuk secara eksplisit menyatakan bahwa Anda mengganti metode virtual. Compiler akan gagal jika metode virtual dasar tidak ada, dan memiliki keterbacaan yang sama dengan menempatkan "virtual" pada kelas turunan.
ShadowChaser
Sebenarnya, dalam C ++ 11 gcc, menulis void hello () menimpa {} di kelas turunannya baik-baik saja karena kelas dasar telah menetapkan bahwa metode hello () adalah virtual. Dengan kata lain, penggunaan kata virtual di kelas turunan tidak diperlukan / wajib, untuk gcc / g ++. (Saya menggunakan versi gcc 4.9.2 pada RPi 3) Tapi itu praktik yang baik untuk memasukkan kata kunci virtual dalam metode kelas turunan pula.
Will

Jawaban:

183

Mereka persis sama. Tidak ada perbedaan di antara mereka selain bahwa pendekatan pertama membutuhkan lebih banyak mengetik dan berpotensi lebih jelas.

James McNellis
sumber
25
Ini benar, tetapi Panduan Portabilitas Mozilla C ++ merekomendasikan selalu menggunakan virtual karena "beberapa kompiler" mengeluarkan peringatan jika Anda tidak. Sayang sekali mereka tidak menyebutkan contoh kompiler seperti itu.
Sergei Tachenov
5
Saya juga akan menambahkan bahwa secara eksplisit menandainya sebagai virtual akan membantu mengingatkan Anda untuk membuat virtual destructor juga.
lfalin
1
Hanya untuk menyebutkan, sama berlaku untuk penghancur virtual
Atul
6
@SergeyTachenov menurut komentar clifford untuk jawabannya sendiri , contoh dari kompiler tersebut adalah armcc.
Ruslan
4
@Rasmi, panduan portabilitas yang baru ada di sini , tetapi sekarang disarankan untuk menggunakan overridekata kunci.
Sergei Tachenov
83

'Virtualness' suatu fungsi disebarkan secara implisit, namun setidaknya satu kompiler yang saya gunakan akan menghasilkan peringatan jika virtualkata kunci tidak digunakan secara eksplisit, jadi Anda mungkin ingin menggunakannya jika hanya untuk membuat kompiler tetap diam.

Dari sudut pandang murni gaya, termasuk virtualkata kunci jelas 'mengiklankan' fakta kepada pengguna bahwa fungsi ini virtual. Ini akan penting bagi siapa pun yang lebih lanjut mengelompokkan B tanpa harus memeriksa definisi A. Untuk hierarki kelas yang dalam, ini menjadi sangat penting.

Clifford
sumber
12
Kompiler mana ini?
James McNellis
35
@ James: armcc (kompiler ARM untuk perangkat ARM)
Clifford
55

Kata virtualkunci tidak diperlukan di kelas turunan. Berikut dokumentasi pendukung, dari C ++ Draft Standard (N3337) (tambang penekanan):

10.3 Fungsi virtual

2 Jika fungsi anggota virtual vfdideklarasikan di kelas Basedan di kelas Derived, berasal langsung atau tidak langsung dari Base, fungsi anggota vfdengan nama yang sama, daftar tipe-parameter (8.3.5), kualifikasi cv, dan kualifikasi ulang ( atau tidak ada yang sama) seperti Base::vfyang dinyatakan, maka Derived::vfjuga virtual ( terlepas atau tidaknya dinyatakan ) dan menimpa Base::vf.

R Sahu
sumber
5
Sejauh ini, inilah jawaban terbaik di sini.
Fantastic Mr Fox
33

Tidak, virtualkata kunci pada penggantian fungsi virtual kelas turunan tidak diperlukan. Tetapi perlu disebutkan jebakan terkait: kegagalan untuk menimpa fungsi virtual.

The kegagalan untuk override terjadi jika Anda berniat untuk menimpa fungsi virtual di kelas turunan, tetapi membuat kesalahan dalam tanda tangan sehingga menyatakan fungsi virtual yang baru dan berbeda. Fungsi ini mungkin kelebihan fungsi kelas dasar, atau mungkin berbeda namanya. Apakah Anda menggunakan virtualkata kunci dalam deklarasi fungsi kelas turunan, kompiler tidak akan dapat memberi tahu bahwa Anda bermaksud menimpa fungsi dari kelas dasar.

Perangkap ini, bagaimanapun, untungnya ditangani oleh fitur bahasa override eksplisit C ++ 11 , yang memungkinkan kode sumber untuk secara jelas menentukan bahwa fungsi anggota dimaksudkan untuk menimpa fungsi kelas dasar:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

Kompiler akan mengeluarkan kesalahan waktu kompilasi dan kesalahan pemrograman akan segera jelas (mungkin fungsi dalam Derived seharusnya diambil floatsebagai argumen).

Lihat WP: C ++ 11 .

Colin D Bennett
sumber
11

Menambahkan kata kunci "virtual" adalah praktik yang baik karena meningkatkan keterbacaan, tetapi tidak perlu. Fungsi dinyatakan virtual di kelas dasar, dan memiliki tanda tangan yang sama di kelas turunan dianggap "virtual" secara default.

Sujay Ghosh
sumber
7

Tidak ada perbedaan untuk kompiler, ketika Anda menulis virtualdi kelas turunan atau menghilangkannya.

Tetapi Anda perlu melihat kelas dasar untuk mendapatkan informasi ini. Karena itu saya akan merekomendasikan untuk menambahkan virtualkata kunci juga di kelas turunan, jika Anda ingin menunjukkan kepada manusia bahwa fungsi ini virtual.

pemain harpa
sumber
2

Ada perbedaan yang cukup besar ketika Anda memiliki template dan mulai mengambil kelas dasar sebagai parameter template:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

Bagian yang menyenangkan dari itu adalah bahwa Anda sekarang dapat mendefinisikan fungsi antarmuka dan non-antarmuka nanti untuk mendefinisikan kelas. Itu berguna untuk antarmuka antar kerja antar pustaka (jangan mengandalkan ini sebagai proses desain standar pustaka tunggal ). Anda tidak perlu mengeluarkan biaya apa pun untuk semua kelas Anda - Anda mungkin typedefakan mendapatkan sesuatu jika Anda mau.

Perhatikan bahwa, jika Anda melakukan ini, Anda mungkin juga ingin mendeklarasikan konstruktor salin / pindahkan sebagai templat: memungkinkan untuk membuat dari antarmuka yang berbeda memungkinkan Anda untuk 'melemparkan' di antara berbagai B<>jenis.

Ini dipertanyakan apakah Anda harus menambahkan dukungan untuk const A&di t_hello(). Alasan yang biasa untuk penulisan ulang ini adalah untuk beralih dari spesialisasi berbasis warisan ke yang berbasis template, sebagian besar karena alasan kinerja. Jika Anda terus mendukung antarmuka lama, Anda hampir tidak dapat mendeteksi (atau menghalangi) penggunaan lama.

lorro
sumber
1

Kata virtualkunci harus ditambahkan ke fungsi dari kelas dasar untuk membuatnya dapat ditimpa. Dalam contoh Anda, struct Aadalah kelas dasar. virtualtidak ada artinya untuk menggunakan fungsi-fungsi tersebut di kelas turunan. Namun, jika Anda ingin kelas turunan Anda juga menjadi kelas dasar itu sendiri, dan Anda ingin fungsi itu dapat ditimpa, maka Anda harus meletakkannya di virtualsana.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Di sini diturunkan Cdari B, jadi Bbukan kelas dasar (itu juga kelas turunan), dan Ckelas turunan. Diagram warisan terlihat seperti ini:

A
^
|
B
^
|
C

Jadi Anda harus meletakkan virtualdi depan fungsi di dalam kelas dasar potensial yang mungkin memiliki anak. virtualmemungkinkan anak-anak Anda untuk mengesampingkan fungsi Anda. Tidak ada yang salah dengan menempatkan fungsi virtualdi depan dalam kelas turunan, tetapi tidak diperlukan. Namun disarankan, karena jika seseorang ingin mewarisi dari kelas turunan Anda, mereka tidak akan senang bahwa metode penimpaan tidak berfungsi seperti yang diharapkan.

Jadi letakkan virtualdi depan fungsi di semua kelas yang terlibat dalam warisan, kecuali Anda tahu pasti bahwa kelas tidak akan memiliki anak yang perlu mengganti fungsi kelas dasar. Ini praktik yang bagus.

Galaksi
sumber
0

Saya pasti akan memasukkan kata kunci Virtual untuk kelas anak, karena

  • saya. Keterbacaan.
  • ii. Kelas anak saya diturunkan lebih jauh, Anda tidak ingin konstruktor dari kelas turunan lebih lanjut memanggil fungsi virtual ini.
pengguna2264698
sumber
1
Saya pikir maksudnya bahwa tanpa menandai fungsi anak sebagai virtual, seorang programmer yang berasal dari kelas anak di kemudian hari mungkin tidak menyadari bahwa fungsi tersebut sebenarnya virtual (karena ia tidak pernah melihat kelas dasar) dan berpotensi menyebutnya sebagai konstruksi ( yang mungkin atau mungkin tidak melakukan hal yang benar).
PfhorSlayer