Bisakah fungsi virtual memiliki parameter default?


164

Jika saya mendeklarasikan kelas dasar (atau kelas antarmuka) dan menentukan nilai default untuk satu atau lebih parameternya, apakah kelas turunan harus menentukan default yang sama dan jika tidak, default mana yang akan terwujud dalam kelas turunan?

Tambahan: Saya juga tertarik dengan bagaimana hal ini dapat ditangani di berbagai kompiler dan masukan apa pun tentang praktik "disarankan" dalam skenario ini.


1
Ini sepertinya hal yang mudah untuk diuji. Sudahkah Anda mencobanya?
dan dan

22
Saya sedang dalam proses mencobanya tetapi saya belum menemukan informasi konkret tentang bagaimana "didefinisikan" perilaku akan jadi saya akhirnya akan menemukan jawaban untuk kompiler spesifik saya tetapi itu tidak akan memberi tahu saya jika semua kompiler akan melakukan hal yang sama benda. Saya juga tertarik dengan praktik yang disarankan.
Arnold Spence

1
Perilaku ini didefinisikan dengan baik, dan saya ragu Anda akan menemukan kompiler yang salah (well, mungkin jika Anda menguji gcc 1.x, atau VC ++ 1.0 atau sesuatu seperti itu). Latihan yang disarankan tidak dilakukan.
Jerry Coffin

Jawaban:


213

Virtual mungkin memiliki default. Default di kelas dasar tidak diwarisi oleh kelas turunan.

Default mana yang digunakan - yaitu, kelas dasar 'atau kelas turunan' - ditentukan oleh tipe statis yang digunakan untuk membuat panggilan ke fungsi. Jika Anda memanggil objek kelas dasar, penunjuk atau referensi, standar yang ditunjukkan dalam kelas dasar digunakan. Sebaliknya, jika Anda memanggil objek kelas turunan, penunjuk atau referensi standar yang ditunjukkan dalam kelas turunan digunakan. Ada contoh di bawah kutipan Standar yang menunjukkan ini.

Beberapa kompiler mungkin melakukan sesuatu yang berbeda, tetapi inilah yang dikatakan oleh Standar C ++ 03 dan C ++ 11:

8.3.6.10:

Panggilan fungsi virtual (10.3) menggunakan argumen default dalam deklarasi fungsi virtual yang ditentukan oleh tipe statis dari pointer atau referensi yang menunjukkan objek. Fungsi utama dalam kelas turunan tidak memperoleh argumen default dari fungsi yang ditimpanya. Contoh:

struct A {
  virtual void f(int a = 7);
};
struct B : public A {
  void f(int a);
};
void m()
{
  B* pb = new B;
  A* pa = pb;
  pa->f(); //OK, calls pa->B::f(7)
  pb->f(); //error: wrong number of arguments for B::f()
}

Berikut adalah contoh program untuk menunjukkan default apa yang diambil. Saya menggunakan structs di sini daripada classes hanya untuk singkatnya - classdan structpersis sama di hampir setiap cara kecuali visibilitas standar.

#include <string>
#include <sstream>
#include <iostream>
#include <iomanip>

using std::stringstream;
using std::string;
using std::cout;
using std::endl;

struct Base { virtual string Speak(int n = 42); };
struct Der : public Base { string Speak(int n = 84); };

string Base::Speak(int n) 
{ 
    stringstream ss;
    ss << "Base " << n;
    return ss.str();
}

string Der::Speak(int n)
{
    stringstream ss;
    ss << "Der " << n;
    return ss.str();
}

int main()
{
    Base b1;
    Der d1;

    Base *pb1 = &b1, *pb2 = &d1;
    Der *pd1 = &d1;
    cout << pb1->Speak() << "\n"    // Base 42
        << pb2->Speak() << "\n"     // Der 42
        << pd1->Speak() << "\n"     // Der 84
        << endl;
}

Output dari program ini (pada MSVC10 dan GCC 4.4) adalah:

Base 42
Der 42
Der 84

Terima kasih untuk referensi, yang memberi tahu saya perilaku yang bisa saya harapkan secara wajar di kompiler (saya harap).
Arnold Spence

Ini adalah koreksi terhadap ringkasan saya sebelumnya: Saya akan menerima jawaban ini untuk referensi dan menyebutkan bahwa rekomendasi kolektif adalah tidak apa-apa untuk memiliki parameter default di fungsi virtual selama mereka tidak mengubah parameter default yang sebelumnya ditentukan dalam leluhur kelas.
Arnold Spence

Saya menggunakan gcc 4.8.1 dan saya tidak mendapatkan kesalahan kompilasi "jumlah argumen salah" !!! Butuh satu setengah hari untuk menemukan bug ...
steffen

2
Tetapi apakah ada alasan untuk itu? Mengapa ditentukan oleh tipe statis?
user1289

2
Dentang rapi memperlakukan parameter default pada metode virtual sebagai sesuatu yang tidak diinginkan dan mengeluarkan peringatan tentang itu: github.com/llvm-mirror/clang-tools-extra/blob/master/clang-tidy/…
Martin Pecka

38

Ini adalah topik dari salah satu posting Guru of the Week awal Herb Sutter .

Hal pertama yang dia katakan tentang masalah ini adalah JANGAN MELAKUKANNYA.

Secara lebih rinci, ya, Anda dapat menentukan parameter default yang berbeda. Mereka tidak akan bekerja dengan cara yang sama seperti fungsi virtual. Fungsi virtual disebut pada tipe dinamis objek, sedangkan nilai parameter default didasarkan pada tipe statis.

Diberikan

class A {
    virtual void foo(int i = 1) { cout << "A::foo" << i << endl; }
};
class B: public A {
    virtual void foo(int i = 2) { cout << "B::foo" << i << endl; }
};
void test() {
A a;
B b;
A* ap = &b;
a.foo();
b.foo();
ap->foo();
}

Anda harus mendapatkan A :: foo1 B :: foo2 B :: foo1


7
Terima kasih. A "Jangan lakukan itu" dari Herb Sutter memiliki bobot.
Arnold Spence

2
@ArnoldSpence, sebenarnya Herb Sutter melampaui rekomendasi ini. Dia percaya antarmuka tidak boleh mengandung metode virtual sama sekali: gotw.ca/publications/mill18.htm . Setelah metode Anda konkret dan tidak dapat (tidak boleh) ditimpa, aman untuk memberi mereka parameter default.
Mark Ransom

1
Saya percaya apa yang ia maksudkan dengan "jangan lakukan itu " adalah "jangan ubah nilai default dari parameter default" dalam metode override, bukan "jangan tentukan parameter default dalam metode virtual"
Weipeng L

6

Ini adalah ide yang buruk, karena argumen default yang Anda peroleh akan tergantung pada tipe statis objek, sedangkan virtualfungsi yang dikirim akan tergantung pada tipe dinamis .

Dengan kata lain, ketika Anda memanggil suatu fungsi dengan argumen default, argumen default diganti pada waktu kompilasi, terlepas dari apakah fungsi tersebut virtualatau tidak.

@cppcoder ditawarkan contoh berikut dalam bukunya [ditutup] pertanyaan :

struct A {
    virtual void display(int i = 5) { std::cout << "Base::" << i << "\n"; }
};
struct B : public A {
    virtual void display(int i = 9) override { std::cout << "Derived::" << i << "\n"; }
};

int main()
{
    A * a = new B();
    a->display();

    A* aa = new A();
    aa->display();

    B* bb = new B();
    bb->display();
}

Yang menghasilkan output berikut:

Derived::5
Base::5
Derived::9

Dengan bantuan penjelasan di atas, mudah diketahui alasannya. Pada waktu kompilasi, kompiler menggantikan argumen default dari fungsi anggota dari tipe statis dari pointer, membuat mainfungsi setara dengan yang berikut:

    A * a = new B();
    a->display(5);

    A* aa = new A();
    aa->display(5);

    B* bb = new B();
    bb->display(9);

4

Seperti yang dapat Anda lihat dari jawaban lain, ini adalah topik yang rumit. Alih-alih mencoba melakukan ini atau memahami apa yang dilakukannya (jika Anda harus bertanya sekarang, pengelola harus meminta atau mencarinya setahun dari sekarang).

Sebagai gantinya, buat fungsi publik non-virtual di kelas dasar dengan parameter default. Kemudian ia memanggil fungsi virtual pribadi atau yang dilindungi yang tidak memiliki parameter default dan ditimpa dalam kelas anak sesuai kebutuhan. Maka Anda tidak perlu khawatir tentang rinciannya dan kodenya sangat jelas.


1
Sama sekali tidak rumit. Parameter default ditemukan bersama dengan resolusi nama. Mereka mengikuti aturan yang sama.
Edward Strange

4

Ini adalah salah satu yang Anda mungkin dapat mencari tahu dengan cukup baik dengan pengujian (yaitu, itu adalah bagian yang cukup utama dari bahasa yang sebagian besar kompiler hampir pasti melakukannya dengan benar dan kecuali jika Anda melihat perbedaan antara kompiler, output mereka dapat dianggap cukup otoritatif).

#include <iostream>

struct base { 
    virtual void x(int a=0) { std::cout << a; }
    virtual ~base() {}
};

struct derived1 : base { 
    void x(int a) { std:: cout << a; }
};

struct derived2 : base { 
    void x(int a = 1) { std::cout << a; }
};

int main() { 
    base *b[3];
    b[0] = new base;
    b[1] = new derived1;
    b[2] = new derived2;

    for (int i=0; i<3; i++) {
        b[i]->x();
        delete b[i];
    }

    derived1 d;
    // d.x();       // won't compile.
    derived2 d2;
    d2.x();
    return 0;
}

4
@ GMan: [Hati-hati tampak tidak bersalah] Apa yang bocor? :-)
Jerry Coffin

Saya pikir dia merujuk pada kurangnya destruktor virtual. Tetapi dalam hal ini tidak akan bocor.
John Dibling

1
@ Jerry, destructor telah menjadi virtual jika Anda menghapus objek turunan melalui pointer kelas dasar. Kalau tidak, destruktor kelas dasar akan dipanggil untuk semuanya. Dalam hal ini tidak masalah karena tidak ada destruktor. :-)
chappar

2
@ John: Awalnya tidak ada penghapusan, itulah yang saya maksud. Saya benar-benar mengabaikan kurangnya destruktor virtual. Dan ... @ chappar: Tidak, tidak apa-apa. Itu harus memiliki destruktor virtual untuk dihapus melalui kelas dasar, atau Anda mendapatkan perilaku yang tidak terdefinisi. (Kode ini memiliki perilaku yang tidak terdefinisi.) Tidak ada hubungannya dengan data atau destruktor yang dimiliki kelas turunan.
GManNickG

@ Chappar: Kode aslinya tidak menghapus apa pun. Meskipun sebagian besar tidak relevan dengan pertanyaan yang ada, saya juga menambahkan virtual dtor ke kelas dasar - dengan dtor sepele, ini jarang penting, tetapi GM sepenuhnya benar bahwa tanpanya, kode tersebut memiliki UB.
Jerry Coffin

4

Sebagai jawaban lain telah merinci, itu ide buruk. Namun karena tidak ada yang menyebutkan solusi sederhana dan efektif, ini dia: Konversikan parameter Anda menjadi struct dan kemudian Anda dapat memiliki nilai default menjadi anggota struct!

Jadi alih-alih,

//bad idea
virtual method1(int x = 0, int y = 0, int z = 0)

melakukan hal ini,

//good idea
struct Param1 {
  int x = 0, y = 0, z = 0;
};
virtual method1(const Param1& p)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.