Perché una funzione sovrascritta nella classe derivata nasconde altri sovraccarichi della classe base?


219

Considera il codice:

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

Ho ricevuto questo errore:

> g ++ -pedantic -Os test.cpp -o test
test.cpp: nella funzione `int main () ':
test.cpp: 31: errore: nessuna funzione corrispondente per la chiamata a `Derived :: gogo (int) '
test.cpp: 21: nota: i candidati sono: void virtuale Derivato :: gogo (int *) 
test.cpp: 33: 2: avviso: nessuna nuova riga alla fine del file
> Codice di uscita: 1

Qui, la funzione della classe Derivata sta eclissando tutte le funzioni con lo stesso nome (non firma) nella classe base. In qualche modo, questo comportamento di C ++ non sembra OK. Non polimorfico.



8
domanda geniale, l'ho scoperto anche di recente
Matt Joiner,

11
Penso che Bjarne (dal link pubblicato da Mac) lo metta meglio in una frase: "In C ++, non c'è sovraccarico tra gli ambiti - gli ambiti di classe derivati ​​non fanno eccezione a questa regola generale".
sivabudh,

7
@Ashish Quel link è interrotto. Ecco quello corretto (per ora) - stroustrup.com/bs_faq2.html#overloadderived
nsane,

3
Inoltre, volevo sottolineare che obj.Base::gogo(7);funziona ancora chiamando la funzione nascosta.
Forumulator

Risposte:


406

A giudicare dal testo della tua domanda (hai usato la parola "nascondi"), sai già cosa sta succedendo qui. Il fenomeno si chiama "nascondere il nome". Per qualche motivo, ogni volta che qualcuno fa una domanda sul perché si verifica il nascondimento dei nomi, le persone che rispondono affermano che questo si chiama "nascondimento dei nomi" e spiegano come funziona (cosa che probabilmente già conosci), o spiegano come sovrascriverlo (che tu mai chiesto), ma nessuno sembra preoccuparsi di affrontare l'attuale domanda "perché".

La decisione, la logica alla base del nascondimento del nome, ovvero perché è stata effettivamente progettata in C ++, è quella di evitare alcuni comportamenti controintuitivi, imprevisti e potenzialmente pericolosi che potrebbero verificarsi se l'insieme ereditato di funzioni sovraccariche fosse permesso di mescolarsi con l'insieme corrente di sovraccarichi nella classe data. Probabilmente sai che in C ++ la risoluzione del sovraccarico funziona scegliendo la migliore funzione dal gruppo di candidati. Questo viene fatto abbinando i tipi di argomenti ai tipi di parametri. Le regole di abbinamento potrebbero essere complicate a volte e spesso portano a risultati che potrebbero essere percepiti come illogici da un utente non preparato. L'aggiunta di nuove funzioni a un set di quelle precedentemente esistenti potrebbe comportare uno spostamento piuttosto drastico dei risultati della risoluzione del sovraccarico.

Ad esempio, supponiamo che la classe base Babbia una funzione membro fooche accetta un parametro di tipo void *e che tutte le chiamate a foo(NULL)vengono risolte B::foo(void *). Supponiamo che non ci sia un nome nascosto e questo B::foo(void *)è visibile in molte classi diverse da cui discendono B. Tuttavia, diciamo che in alcuni discendenti [indiretti, remoti] Ddella classe viene definita Buna funzione foo(int). Ora, senza nome nascondersi Dha sia foo(void *)e foo(int)visibile e partecipando nella risoluzione di sovraccarico. A quale funzione si rivolgeranno le chiamate foo(NULL), se effettuate attraverso un oggetto di tipo D? Si risolveranno D::foo(int), poiché intè una corrispondenza migliore per lo zero integrale (ad esNULL) rispetto a qualsiasi tipo di puntatore. Quindi, in tutta la gerarchia, le chiamate si foo(NULL)risolvono in una funzione, mentre in D(e sotto) si risolvono improvvisamente in un'altra.

Un altro esempio è riportato in The Design and Evolution of C ++ , pagina 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*)
}

Senza questa regola, lo stato di b verrebbe parzialmente aggiornato, portando alla suddivisione.

Questo comportamento è stato ritenuto indesiderabile quando è stata progettata la lingua. Come approccio migliore, è stato deciso di seguire la specifica "nascondere il nome", nel senso che ogni classe inizia con un "foglio pulito" rispetto al nome di ogni metodo che dichiara. Per sovrascrivere questo comportamento, è richiesta un'azione esplicita da parte dell'utente: originariamente una rideclarazione dei metodi ereditati (attualmente deprecati), ora un uso esplicito di using-statement.

Come hai correttamente osservato nel tuo post originale (mi riferisco all'osservazione "Non polimorfico"), questo comportamento potrebbe essere visto come una violazione della relazione IS-A tra le classi. Questo è vero, ma apparentemente allora si decise che alla fine il nascondere il nome si sarebbe rivelato un male minore.


22
Sì, questa è una vera risposta alla domanda. Grazie. Anche io ero curioso.
Onnipotente il

4
Bella risposta! Inoltre, dal punto di vista pratico, la compilazione sarebbe probabilmente molto più lenta se la ricerca del nome dovesse arrivare fino in cima ogni volta.
Ha disegnato la sala il

6
(Vecchia risposta, lo so.) Ora nullptrobietterei al tuo esempio dicendo "se vuoi chiamare la void*versione, dovresti usare un tipo di puntatore". C'è un esempio diverso in cui questo può essere negativo?
GManNickG,

3
Il nome nascosto non è proprio malvagio. La relazione "is-a" è ancora presente e disponibile tramite l'interfaccia di base. Quindi forse d->foo()non ti procurerà "Is-a Base", ma lo static_cast<Base*>(d)->foo() farà , incluso l'invio dinamico.
Kerrek SB,

12
Questa risposta non è utile perché l'esempio fornito si comporta allo stesso modo con o senza nascondersi: D :: foo (int) verrà chiamato perché è una corrispondenza migliore o perché ha nascosto B: foo (int).
Richard Wolf,

46

Le regole di risoluzione dei nomi affermano che la ricerca dei nomi si interrompe nel primo ambito in cui viene trovato un nome corrispondente. A quel punto, entrano in gioco le regole di risoluzione del sovraccarico per trovare la migliore corrispondenza delle funzioni disponibili.

In questo caso, gogo(int*)si trova (da solo) nell'ambito della classe Derived e, poiché non esiste una conversione standard da int a int *, la ricerca ha esito negativo.

La soluzione è inserire le dichiarazioni Base tramite una dichiarazione using nella classe Derivata:

using Base::gogo;

... consentirebbe alle regole di ricerca del nome di trovare tutti i candidati e quindi la risoluzione del sovraccarico procederebbe come previsto.


10
OP: "Perché una funzione sostituita nella classe derivata nasconde altri sovraccarichi della classe base?" Questa risposta: "Perché lo fa".
Richard Wolf,

12

Questo è "In base alla progettazione". In C ++ la risoluzione del sovraccarico per questo tipo di metodo funziona come segue.

  • Partendo dal tipo di riferimento e poi passando al tipo di base, trova il primo tipo che ha un metodo chiamato "gogo"
  • Considerando solo i metodi denominati "gogo" su quel tipo, si trova un sovraccarico corrispondente

Poiché Derived non ha una funzione corrispondente denominata "gogo", la risoluzione del sovraccarico non riesce.


2

Nascondere i nomi ha senso perché impedisce le ambiguità nella risoluzione dei nomi.

Considera questo codice:

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

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

Derived dobj;

Se Base::func(float)non fosse nascosto da Derived::func(double)Derived, chiameremmo la funzione della classe base quando si chiama dobj.func(0.f), anche se un float può essere promosso a doppio.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.