Mengapa fungsi yang ditimpa dalam kelas turunan menyembunyikan kelebihan lainnya dari kelas dasar?

219

Pertimbangkan kodenya:

#include <stdio.h>

class Base {
public: 
    virtual void gogo(int a){
        printf(" Base :: gogo (int) \n");
    };

    virtual void gogo(int* a){
        printf(" Base :: gogo (int*) \n");
    };
};

class Derived : public Base{
public:
    virtual void gogo(int* a){
        printf(" Derived :: gogo (int*) \n");
    };
};

int main(){
    Derived obj;
    obj.gogo(7);
}

Mendapat kesalahan ini:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: Dalam fungsi `int main () ':
test.cpp: 31: error: tidak ada fungsi yang cocok untuk panggilan ke `Derived :: gogo (int) '
test.cpp: 21: note: kandidat adalah: virtual void Derived :: gogo (int *) 
test.cpp: 33: 2: peringatan: tidak ada baris baru di akhir file
> Kode keluar: 1

Di sini, fungsi kelas turunan adalah melampaui semua fungsi dengan nama yang sama (bukan tanda tangan) di kelas dasar. Entah bagaimana, perilaku C ++ ini tidak terlihat OK. Bukan polimorfik.

Aman Aggarwal
sumber
8
pertanyaan brilian, saya hanya menemukan ini baru-baru ini juga
Matt Joiner
11
Saya pikir Bjarne (dari tautan yang diposting Mac) mengatakannya dengan terbaik dalam satu kalimat: "Di C ++, tidak ada kelebihan beban di seluruh cakupan - cakupan kelas turunan tidak terkecuali aturan umum ini."
sivabudh
7
@Ashish Tautan itu rusak. Inilah yang benar (sampai sekarang) - stroustrup.com/bs_faq2.html#overloadderived
nsane
3
Juga, ingin menunjukkan bahwa obj.Base::gogo(7);masih berfungsi dengan memanggil fungsi tersembunyi.
forumulator

Jawaban:

406

Menilai dari kata-kata pertanyaan Anda (Anda menggunakan kata "sembunyikan"), Anda sudah tahu apa yang sedang terjadi di sini. Fenomena itu disebut "nama bersembunyi". Untuk beberapa alasan, setiap kali seseorang mengajukan pertanyaan tentang mengapa penyembunyian nama terjadi, orang yang menjawab mengatakan bahwa ini disebut "penyembunyian nama" dan menjelaskan cara kerjanya (yang mungkin sudah Anda ketahui), atau menjelaskan cara menimpanya (yang Anda tidak pernah bertanya tentang), tetapi tampaknya tidak ada yang peduli untuk menjawab pertanyaan "mengapa" yang sebenarnya.

Keputusan, alasan di balik nama bersembunyi, yaitu mengapa itu sebenarnya dirancang ke dalam C ++, adalah untuk menghindari perilaku yang berlawanan dengan intuisi, tak terduga dan berpotensi berbahaya yang mungkin terjadi jika rangkaian fungsi kelebihan beban yang diwarisi dibiarkan bercampur dengan perangkat saat ini. kelebihan di kelas yang diberikan. Anda mungkin tahu bahwa dalam C ++ resolusi kelebihan bekerja dengan memilih fungsi terbaik dari sekumpulan kandidat. Ini dilakukan dengan mencocokkan jenis argumen dengan jenis parameter. Aturan-aturan yang cocok kadang-kadang bisa menjadi rumit, dan seringkali mengarah pada hasil yang mungkin dianggap tidak logis oleh pengguna yang tidak siap. Menambahkan fungsi baru ke satu set yang sudah ada sebelumnya mungkin menghasilkan perubahan yang agak drastis dalam hasil resolusi kelebihan beban.

Sebagai contoh, katakanlah kelas dasar Bmemiliki fungsi anggotafoo yang mengambil parameter tipe void *, dan semua panggilan untuk foo(NULL)diselesaikan B::foo(void *). Katakanlah tidak ada nama yang disembunyikan dan ini B::foo(void *)terlihat di banyak kelas yang berbeda B. Namun, katakanlah dalam beberapa keturunan [, remote tidak langsung] Dkelas Bfungsi foo(int)didefinisikan. Sekarang, tanpa menyembunyikan nama Dmemiliki keduanya foo(void *)dan foo(int)terlihat serta berpartisipasi dalam resolusi kelebihan. Fungsi mana yang akan foo(NULL)diselesaikan oleh panggilan , jika dilakukan melalui objek bertipe D? Mereka akan menyelesaikannya D::foo(int), karena intmerupakan kecocokan yang lebih baik untuk integral nol (yaituNULL) daripada jenis pointer apa pun. Jadi, di seluruh hierarki panggilan untuk foo(NULL)menyelesaikan ke satu fungsi, sementara di D(dan di bawah) mereka tiba-tiba memutuskan untuk yang lain.

Contoh lain diberikan dalam Desain dan Evolusi C ++ , halaman 77:

class Base {
    int x;
public:
    virtual void copy(Base* p) { x = p-> x; }
};

class Derived{
    int xx;
public:
    virtual void copy(Derived* p) { xx = p->xx; Base::copy(p); }
};

void f(Base a, Derived b)
{
    a.copy(&b); // ok: copy Base part of b
    b.copy(&a); // error: copy(Base*) is hidden by copy(Derived*)
}

Tanpa aturan ini, status b akan diperbarui sebagian, yang mengarah ke pemotongan.

Perilaku ini dianggap tidak diinginkan ketika bahasa dirancang. Sebagai pendekatan yang lebih baik, diputuskan untuk mengikuti spesifikasi "name hiding", yang berarti bahwa setiap kelas dimulai dengan "clean sheet" sehubungan dengan setiap nama metode yang dinyatakannya. Untuk mengesampingkan perilaku ini, diperlukan tindakan eksplisit dari pengguna: awalnya merupakan deklarasi ulang dari metode yang diwarisi (saat ini tidak digunakan lagi), sekarang merupakan penggunaan eksplisit dari deklarasi penggunaan.

Seperti yang Anda amati dengan benar di posting asli Anda (saya merujuk pada komentar "Bukan polimorfik"), perilaku ini mungkin dilihat sebagai pelanggaran hubungan IS-A antara kelas-kelas. Ini benar, tetapi tampaknya saat itu diputuskan bahwa pada akhirnya nama persembunyian akan terbukti menjadi kejahatan yang lebih kecil.

Semut
sumber
22
Ya, ini adalah jawaban nyata untuk pertanyaan itu. Terima kasih. Saya juga penasaran.
Mahakuasa
4
Jawaban bagus! Juga, sebagai masalah praktis, kompilasi mungkin akan menjadi jauh lebih lambat jika pencarian nama harus dilakukan terus ke atas setiap kali.
Drew Hall
6
(Jawaban lama, saya tahu.) Sekarang nullptrsaya akan keberatan dengan contoh Anda dengan mengatakan "jika Anda ingin memanggil void*versi, Anda harus menggunakan tipe pointer". Apakah ada contoh berbeda di mana ini bisa buruk?
GManNickG
3
Nama persembunyiannya tidak terlalu jahat. Hubungan "is-a" masih ada, dan tersedia melalui antarmuka dasar. Jadi mungkin d->foo()tidak akan memberi Anda "Is-a Base", tetapi static_cast<Base*>(d)->foo() akan , termasuk pengiriman dinamis.
Kerrek SB
12
Jawaban ini tidak membantu karena contoh yang diberikan berperilaku sama dengan atau tanpa bersembunyi: D :: foo (int) akan dipanggil karena itu adalah kecocokan yang lebih baik atau karena telah menyembunyikan B: foo (int).
Richard Wolf
46

Aturan resolusi nama mengatakan bahwa pencarian nama berhenti di ruang lingkup pertama di mana nama yang cocok ditemukan. Pada titik itu, aturan resolusi kelebihan muatan masuk untuk menemukan kecocokan terbaik dari fungsi yang tersedia.

Dalam kasus ini, gogo(int*)ditemukan (sendiri) dalam lingkup kelas Derived, dan karena tidak ada konversi standar dari int ke int *, pencarian gagal.

Solusinya adalah membawa deklarasi Base melalui deklarasi menggunakan di kelas Derived:

using Base::gogo;

... akan mengizinkan aturan pencarian nama untuk menemukan semua kandidat dan dengan demikian resolusi kelebihan akan dilanjutkan seperti yang Anda harapkan.

Drew Hall
sumber
10
OP: "Mengapa fungsi yang diganti dalam kelas turunan menyembunyikan kelebihan beban lainnya dari kelas dasar?" Jawaban ini: "Karena itu benar".
Richard Wolf
12

Ini adalah "By Design". Dalam C ++ resolusi kelebihan untuk jenis metode ini bekerja seperti berikut ini.

  • Mulai dari jenis referensi dan kemudian pergi ke jenis dasar, cari jenis pertama yang memiliki metode bernama "gogo"
  • Mempertimbangkan hanya metode bernama "gogo" pada tipe itu yang menemukan kelebihan yang cocok

Karena Berasal tidak memiliki fungsi pencocokan bernama "gogo", resolusi kelebihan gagal.

JaredPar
sumber
2

Menyembunyikan nama masuk akal karena mencegah ambiguitas dalam resolusi nama.

Pertimbangkan kode ini:

class Base
{
public:
    void func (float x) { ... }
}

class Derived: public Base
{
public:
    void func (double x) { ... }
}

Derived dobj;

Jika Base::func(float)tidak disembunyikan Derived::func(double)di dalam Derived, kita akan memanggil fungsi kelas dasar saat memanggil dobj.func(0.f), meskipun float dapat dipromosikan menjadi double.

Referensi: http://bastian.rieck.ru/blog/posts/2016/name_hiding_cxx/

Sandeep Singh
sumber