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

StackOverflow https://stackoverflow.com/questions/1628768

  •  06-07-2019
  •  | 
  •  

Domanda

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: In function `int main()':
test.cpp:31: error: no matching function for call to `Derived::gogo(int)'
test.cpp:21: note: candidates are: virtual void Derived::gogo(int*) 
test.cpp:33:2: warning: no newline at end of file
>Exit code: 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.

È stato utile?

Soluzione

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 nasconde il nome, le persone che rispondono affermano che questo si chiama "nascondere il nome". e spiega come funziona (cosa che probabilmente già conosci), o spiega come sovrascriverlo (di cui non hai mai chiesto), ma a nessuno sembra interessare l'effettivo "perché". domanda.

La decisione, la logica alla base del nascondere il nome, ovvero perché in realtà è stata progettata in C ++, è quella di evitare alcuni comportamenti controintuitivi, imprevisti e potenzialmente pericolosi che potrebbero verificarsi se l'insieme ereditato di sovraccarico le funzioni potevano mescolarsi con l'attuale serie 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 B abbia una funzione membro foo che accetta un parametro di tipo void * e tutte le chiamate in foo (NULL) sono stati risolti in B :: foo (void *) . Supponiamo che non ci sia un nome nascosto e questo B :: foo (void *) è visibile in molte classi diverse che discendono da B . Tuttavia, diciamo in alcuni [indiretti, remoti] discendenti D della classe B è definita una funzione foo (int) . Ora, senza nascondere il nome D è visibile sia foo (void *) sia foo (int) e partecipa alla risoluzione del sovraccarico. A quale funzione risolveranno le chiamate a foo (NULL) , se effettuate tramite un oggetto di tipo D ? Si risolveranno in D :: foo (int) , poiché int è una corrispondenza migliore per lo zero integrale (cioè NULL ) rispetto a qualsiasi tipo di puntatore . Quindi, in tutta la gerarchia le chiamate a foo (NULL) si 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 a divisione.

Questo comportamento è stato ritenuto indesiderabile al momento della progettazione della lingua. Come approccio migliore, è stato deciso di seguire il "nome nascosto" specifica, nel senso che ogni classe inizia con un "foglio pulito". rispetto al nome di ciascun metodo che dichiara. Al fine di ignorare questo comportamento, è richiesta un'azione esplicita da parte dell'utente: originariamente una rideclarazione dei metodi ereditati (attualmente deprecati), ora un uso esplicito di using-dichiarazioni.

Come hai correttamente osservato nel tuo post originale (mi riferisco all'osservazione "Non polimorfico"), questo comportamento potrebbe essere visto come una violazione del rapporto 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.

Altri suggerimenti

Le regole di risoluzione dei nomi indicano 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 *) viene trovato (da solo) nell'ambito della classe Derived e, poiché non esiste una conversione standard da int a int *, la ricerca non riesce.

La soluzione consiste nel portare 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.

Questo è " Per 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 trova un sovraccarico corrispondente

Poiché Derived non ha una funzione corrispondente denominata " gogo " ;, la risoluzione di sovraccarico fallisce.

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) in Derived, chiameremmo la funzione della classe base quando chiamiamo dobj.func (0.f) , anche se un float può essere promosso a doppio.

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top