Türetilmiş sınıftaki geçersiz kılınmış bir işlev neden temel sınıfın diğer aşırı yüklenmelerini gizliyor?


220

Kodu düşünün:

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

Bu hatayı aldım:

> g ++ -pedantic -Os testi.cpp -o testi
test.cpp: `int main () 'işlevinde:
test.cpp: 31: hata: `Derived :: gogo (int) 'çağrısı için eşleşen işlev yok
test.cpp: 21: not: adaylar: sanal void Türetilmiş :: gogo (int *) 
test.cpp: 33: 2: uyarı: dosyanın sonunda yeni satır yok
> Çıkış kodu: 1

Burada, Türetilmiş sınıfın işlevi, temel sınıfta aynı ada (imza değil) tüm işlevleri ekler. Her nasılsa, bu C ++ davranışı iyi görünmüyor. Polimorfik değil.



8
parlak soru, ben sadece bu son zamanlarda da keşfettim
Matt Joiner

11
Bjarne (Mac yayınlanan bağlantıdan) bir cümle en iyi koymak düşünüyorum: "C ++, kapsamları arasında aşırı yükleme yoktur - türetilmiş sınıf kapsamları bu genel kural bir istisna değildir."
sivabudh

7
@Ashish Bu bağlantı koptu. İşte doğru olanı (şu andan itibaren) - stroustrup.com/bs_faq2.html#overloadderived
nsane

3
Ayrıca, obj.Base::gogo(7);gizli işlevi çağırarak hala işe yaradığını belirtmek istedim .
forumülatör

Yanıtlar:


406

Sorunuzun ifadesine bakarak ("sakla" kelimesini kullandınız), burada neler olduğunu zaten biliyorsunuz. Bu fenomene "isim gizleme" denir. Herhangi bir nedenle, birisi ad gizlemenin neden gerçekleştiği hakkında her soru sorduğunda , yanıt veren kişiler ya "ad gizleme" adını verdiğini söyler ve nasıl çalıştığını (muhtemelen zaten biliyorsunuz) ya da nasıl geçersiz kılacağını açıklar ( hiç sormadı), ama hiç kimse gerçek "neden" sorusunu ele almak umurunda görünmüyor.

Karar, ismin gizlenmesinin ardındaki mantık, yani aslında neden C ++ 'da tasarlandığını, miras alınan aşırı yüklenmiş fonksiyonlar setinin mevcut set ile karışmasına izin verilebilecek belirli mantıksız, öngörülemeyen ve potansiyel olarak tehlikeli davranışlardan kaçınmaktır. verilen sınıfta aşırı yüklenmeler. Muhtemelen C ++ aşırı yük çözünürlüğünün aday setinden en iyi işlevi seçerek çalıştığını biliyorsunuzdur. Bu, argüman türlerini parametre türleriyle eşleştirerek yapılır. Eşleşen kurallar zaman zaman karmaşık olabilir ve genellikle hazırlıksız bir kullanıcı tarafından mantıksız olarak algılanabilecek sonuçlara yol açabilir. Önceden var olan bir dizi gruba yeni işlevler eklemek, aşırı yük çözünürlüğü sonuçlarında oldukça ciddi bir kaymaya neden olabilir.

Örneğin, temel sınıfın tür parametresini alan bir Büye işlevi fooolduğunu void *ve tüm çağrıların foo(NULL)çözümlendiğini varsayalım B::foo(void *). Diyelim ki hiçbir isim saklanmadı ve bu B::foo(void *), inen birçok farklı sınıfta görülebilir B. Ancak, diyelim ki bazı [dolaylı, uzak] soyundan Dsınıfın Bbir işlevi foo(int)tanımlanmıştır. Şimdi, adı gizleme olmadan Dhem sahiptir foo(void *)ve foo(int)görünür ve aşırı yük çözünürlükte katılan. Türdeki foo(NULL)bir nesne üzerinden yapıldığında , çağrılar hangi işleve çözümlenecektir D? Çözüleceklerdir D::foo(int), çünkü intintegral sıfır için daha iyi bir eştir (ör.NULL) herhangi bir işaretçi türünden daha fazladır. Yani, hiyerarşi boyunca çağrısındafoo(NULL)bir işleve giderken D(içinde ve altında) aniden diğerine giderler.

Başka bir örnek C ++ Tasarım ve Gelişimi , sayfa 77'de verilmiştir:

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*)
}

Bu kural olmadan, b'nin durumu kısmen güncellenerek dilimlemeye yol açar.

Bu davranış, dil tasarlanırken istenmeyen bir durum olarak kabul edildi. Daha iyi bir yaklaşım olarak, "isim gizleme" şartnamesini izlemeye karar verildi, yani her sınıf, beyan ettiği her yöntem adına göre bir "temiz sayfa" ile başlar. Bu davranışı geçersiz kılmak için kullanıcıdan açık bir eylem gereklidir: aslen miras alınan yöntem (ler) in (şu anda kullanımdan kaldırılmış) bir ifadesi, şimdi açık bir kullanım-bildirimi kullanımı.

Orijinal yayında doğru şekilde gözlemlediğiniz gibi ("Polimorfik değil" ifadesine atıfta bulunuyorum), bu davranış sınıflar arasındaki IS-A ilişkisinin ihlali olarak görülebilir. Bu doğrudur, ama görünüşe göre o zamanlar sonunda isim gizlemenin daha az kötülük olacağına karar verildi.


22
Evet, bu soruya gerçek bir cevap. Teşekkür ederim. Ben de merak ettim.
Şeye Giden

4
Mükemmel cevap! Ayrıca, pratik bir konu olarak, ad araması her seferinde en üste çıkmak zorundaysa derleme çok daha yavaş olacaktır.
Drew Hall

6
(Eski cevap, biliyorum.) Şimdi nullptrörneğinize " void*sürümü çağırmak istiyorsanız, bir işaretçi türü kullanmalısınız " diyerek itiraz edeceğim . Bunun kötü olabileceği farklı bir örnek var mı?
GManNickG

3
Saklanmak ismi gerçekten kötü değil. "İs-a" ilişkisi hala oradadır ve temel arayüz üzerinden kullanılabilir. Bu yüzden belki d->foo()"Is-a Base" elde edemezsiniz , ancak dinamik gönderim dahil static_cast<Base*>(d)->foo() olacaktır .
Kerrek SB

12
Bu cevap yardımcı olmaz çünkü verilen örnek gizlenerek veya gizlenmeden aynı şekilde davranır: D :: foo (int), daha iyi bir eşleşme olduğu veya B: foo (int) gizlendiği için çağrılır.
Richard Wolf

46

Ad çözümleme kuralları, ad aramasının eşleşen bir adın bulunduğu ilk kapsamda durduğunu söyler. Bu noktada, aşırı yük çözünürlüğü kuralları mevcut işlevlerle en iyi eşleşmeyi bulmak için devreye girer.

Bu durumda, gogo(int*)(yalnızca) Türetilmiş sınıf kapsamında bulunur ve int'ten int * 'e standart dönüşüm olmadığından, arama başarısız olur.

Çözüm, türetilen sınıftaki bir beyanı kullanarak Base bildirimlerini getirmektir:

using Base::gogo;

... isim arama kurallarının tüm adayları bulmasına izin verecek ve aşırı yük çözünürlüğü beklediğiniz gibi devam edecekti.


10
OP: "Neden türetilmiş sınıftaki geçersiz kılınmış bir işlev temel sınıfın diğer aşırı yüklenmelerini gizliyor?" Bu cevap: "Çünkü öyle".
Richard Wolf

12

Bu "Tasarım gereğidir". C ++ bu tür bir yöntem için aşırı yük çözünürlüğü aşağıdaki gibi çalışır.

  • Başvuru türünden başlayıp ardından temel türe gitmek için "gogo" adında bir yöntemi olan ilk türü bulun
  • Yalnızca bu türdeki "gogo" adı verilen yöntemleri göz önünde bulundurarak eşleşen bir aşırı yük bulun

Derived, "gogo" adında bir eşleştirme işlevine sahip olmadığından aşırı yük çözünürlüğü başarısız olur.


2

İsim gizleme mantıklıdır çünkü isim çözümlemesindeki belirsizlikleri önler.

Bu kodu düşünün:

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

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

Derived dobj;

Eğer Base::func(float)tarafından gizli değildi Derived::func(double)Türetilmiş Ararken, üs sınıfı işlevini çağırır dobj.func(0.f)bir şamandıra çift terfi edilebilir olsa bile,.

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.