Perché devo accedere ai membri della classe base del modello tramite questo puntatore?


199

Se le classi sottostanti non fossero modelli che potrei semplicemente avere xnella derivedclasse. Tuttavia, con il codice qui sotto, io devo usare this->x. Perché?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}

1
Ah cavolo. Ha qualcosa a che fare con la ricerca del nome. Se qualcuno non risponderà presto, lo cercherò e lo pubblicherò (occupato ora).
templatetypedef

@Ed Swangren: Scusa, mi sono perso tra le risposte offerte quando ho pubblicato questa domanda. Da tempo cercavo la risposta.
Ali,

6
Ciò accade a causa della ricerca del nome in due fasi (che non tutti i compilatori utilizzano per impostazione predefinita) e dei nomi dipendenti. Esistono 3 soluzioni a questo problema, oltre al prefisso xcon this->, ovvero: 1) Usa il prefisso base<T>::x, 2) Aggiungi un'istruzione using base<T>::x, 3) Usa uno switch di compilatore globale che abilita la modalità permissiva. I pro ei contro di queste soluzioni sono descritti in stackoverflow.com/questions/50321788/…
George Robinson,

Risposte:


274

Risposta breve: per creare xun nome dipendente, in modo che la ricerca venga rinviata fino a quando il parametro template non è noto.

Risposta lunga: quando un compilatore vede un modello, dovrebbe eseguire immediatamente alcuni controlli, senza vedere il parametro template. Altri vengono rinviati fino a quando il parametro non è noto. Si chiama compilazione in due fasi e MSVC non lo fa, ma è richiesto dallo standard e implementato dagli altri principali compilatori. Se lo desideri, il compilatore deve compilare il modello non appena lo vede (in una sorta di rappresentazione interna dell'albero di analisi) e rinviare la compilazione dell'istanza fino a dopo.

I controlli eseguiti sul modello stesso, anziché su particolari istanze di esso, richiedono che il compilatore sia in grado di risolvere la grammatica del codice nel modello.

In C ++ (e C), per risolvere la grammatica del codice, a volte è necessario sapere se qualcosa è un tipo o meno. Per esempio:

#if WANT_POINTER
    typedef int A;
#else
    int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }

se A è un tipo, che dichiara un puntatore (senza alcun effetto se non quello di ombreggiare il globale x). Se A è un oggetto, questa è una moltiplicazione (e impedire ad un operatore di sovraccaricarlo è illegale, assegnarlo a un valore). Se è sbagliato, questo errore deve essere diagnosticato nella fase 1 , è definito dallo standard come un errore nel modello , non in una particolare istanza di esso. Anche se il modello non è mai istanziato, se A è un intallora il codice sopra è mal formato e deve essere diagnosticato, proprio come sarebbe se foonon fosse un modello, ma una semplice funzione.

Ora, lo standard dice che i nomi che non dipendono dai parametri del modello devono essere risolvibili nella fase 1. Aqui non è un nome dipendente, si riferisce alla stessa cosa indipendentemente dal tipo T. Quindi deve essere definito prima di definire il modello per essere trovato e verificato nella fase 1.

T::Asarebbe un nome che dipende da T. Non possiamo sapere nella fase 1 se si tratta di un tipo o meno. Il tipo che verrà eventualmente utilizzato come Tin un'istanza molto probabilmente non è ancora definito, e anche se lo fosse non sappiamo quale tipo verrà utilizzato come parametro del modello. Ma dobbiamo risolvere la grammatica per fare i nostri preziosi controlli di fase 1 per modelli mal formati. Quindi lo standard ha una regola per i nomi dipendenti: il compilatore deve presumere che non siano tipi, a meno che non sia qualificato typenameper specificare che sono tipi o utilizzato in determinati contesti non ambigui. Ad esempio template <typename T> struct Foo : T::A {};, T::Aviene utilizzato come classe di base e quindi è inequivocabilmente un tipo. Se Fooè istanziato con un tipo con un membro datiA invece di un tipo nidificato A, si tratta di un errore nel codice che esegue l'istanza (fase 2), non di un errore nel modello (fase 1).

Ma che dire di un modello di classe con una classe base dipendente?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

A è un nome dipendente o no? Con le classi base, qualsiasi nome potrebbe apparire nella classe base. Quindi potremmo dire che A è un nome dipendente e trattarlo come un non tipo. Ciò avrebbe l'effetto indesiderabile che ogni nome in Foo sia dipendente, e quindi ogni tipo usato in Foo (eccetto i tipi incorporati) deve essere qualificato. All'interno di Foo, dovresti scrivere:

typename std::string s = "hello, world";

perché std::stringsarebbe un nome dipendente e quindi assunto come non-tipo se non diversamente specificato. Ahia!

Un secondo problema nel consentire il tuo codice preferito ( return x;) è che anche se Bardefinito in precedenza Foo, e xnon è un membro in quella definizione, qualcuno potrebbe in seguito definire una specializzazione Barper un tipo Baz, tale che Bar<Baz>ha un membro di dati x, e quindi creare un'istanza Foo<Baz>. Quindi, in quell'istanza, il modello restituirebbe il membro dei dati invece di restituire il globale x. O viceversa, se la definizione del modello base di Baravesse x, potrebbero definire una specializzazione senza di essa e il tuo modello cercherebbe un xritorno globale Foo<Baz>. Penso che questo sia stato giudicato altrettanto sorprendente e angosciante del problema che hai, ma è silenzioso sorprendente, invece di lanciare un errore sorprendente.

Per evitare questi problemi, lo standard in effetti afferma che le classi di base dipendenti dei modelli di classe non vengono prese in considerazione per la ricerca se non esplicitamente richiesto. Questo impedisce a tutto di essere dipendente solo perché potrebbe essere trovato in una base dipendente. Ha anche l'effetto indesiderato che stai vedendo: devi qualificare le cose dalla classe base o non sono state trovate. Esistono tre modi comuni per rendere Adipendenti:

  • using Bar<T>::A;nella classe - Aora si riferisce a qualcosa in Bar<T>, quindi dipendente.
  • Bar<T>::A *x = 0;al punto di utilizzo - Ancora una volta, Aè sicuramente in Bar<T>. Questa è una moltiplicazione poiché typenamenon è stata utilizzata, quindi probabilmente un cattivo esempio, ma dovremo attendere fino all'istanza per scoprire se operator*(Bar<T>::A, x)restituisce un valore. Chissà, forse lo fa ...
  • this->A;al punto di utilizzo - Aè un membro, quindi se non lo è Foo, deve essere nella classe base, di nuovo lo standard dice che questo lo rende dipendente.

La compilazione in due fasi è complicata e difficile e introduce alcuni requisiti sorprendenti per la verbosità aggiuntiva nel tuo codice. Ma piuttosto come la democrazia è probabilmente il peggior modo possibile di fare le cose, a parte tutte le altre.

Si potrebbe ragionevolmente sostenere che nel tuo esempio, return x;non ha senso se xè un tipo nidificato nella classe base, quindi la lingua dovrebbe (a) dire che è un nome dipendente e (2) trattarlo come un non tipo, e il tuo codice funzionerebbe senza this->. Nella misura in cui sei vittima di un danno collaterale derivante dalla soluzione a un problema che non si applica al tuo caso, ma c'è ancora il problema della tua classe base che potenzialmente introduce nomi sotto di te che globi ombra, o non hai nomi che pensavi avevano, e invece si trovava un essere globale.

Si potrebbe anche sostenere che il valore predefinito dovrebbe essere l'opposto per i nomi dipendenti (assumere il tipo a meno che in qualche modo non sia specificato come oggetto) o che il valore predefinito dovrebbe essere più sensibile al contesto (in std::string s = "";, std::stringpotrebbe essere letto come un tipo poiché nient'altro rende grammaticale senso, anche se std::string *s = 0;è ambiguo). Ancora una volta, non so bene come siano state concordate le regole. La mia ipotesi è che il numero di pagine di testo che sarebbe richiesto, mitigato contro la creazione di molte regole specifiche per cui i contesti prendono un tipo e quali un non tipo.


1
Ooh, bella risposta dettagliata. Ho chiarito un paio di cose che non mi sono mai preso la briga di alzare lo sguardo. :) +1
jalf

20
@jalf: esiste qualcosa come il C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Domande che verrebbero poste frequentemente tranne che non sei sicuro di voler conoscere la risposta e avere cose più importanti con cui andare d'accordo"?
Steve Jessop,

4
risposta straordinaria, mi chiedo se la domanda potrebbe rientrare nelle domande frequenti.
Matthieu M.

Whoa, possiamo dire enciclopedico? highfive Un punto sottile, però: "Se Foo è istanziato con un tipo che ha un membro di dati A anziché un tipo nidificato A, questo è un errore nel codice che esegue l'istanza (fase 2), non un errore nel modello (fase 1)." Potrebbe essere meglio dire che il modello non è malformato, ma questo potrebbe ancora essere un caso di un presupposto errato o un bug logico da parte di chi scrive il modello. Se l'istanza contrassegnata fosse effettivamente il caso d'uso previsto, il modello sarebbe errato.
Ionoclast Brigham,

1
@JohnH. Dato che diversi compilatori implementano -fpermissiveo simili, sì, è possibile. Non conosco i dettagli di come viene implementata, ma il compilatore deve rimandare la risoluzione xfino a quando non conosce la classe base tempate effettiva T. Quindi, in linea di principio in modalità non permissiva, potrebbe registrare il fatto che lo ha rinviato, rimandarlo, fare la ricerca una volta che ha T, e quando la ricerca riesce a pubblicare il testo che suggerisci. Sarebbe un suggerimento molto accurato se fosse fatto solo nei casi in cui funziona: le possibilità che l'utente intendesse xqualcun altro da un altro ambito sono piuttosto minuscole!
Steve Jessop,

13

(Risposta originale del 10 gennaio 2011)

Penso di aver trovato la risposta: problema GCC: usare un membro di una classe base che dipende da un argomento modello . La risposta non è specifica per gcc.


Aggiornamento: in risposta al commento di mmichael , dalla bozza N3337 della norma C ++ 11:

14.6.2 Nomi dipendenti [temp.dep]
[...]
3 Nella definizione di una classe o di un modello di classe, se una classe di base dipende da un parametro di modello, l'ambito della classe di base non viene esaminato durante la ricerca di nomi non qualificati in il punto di definizione del modello o del membro della classe o durante un'istanza del modello o del membro della classe.

Se "perché lo standard lo dice" conta come una risposta, non lo so. Ora possiamo chiederci perché lo standard imponga che, ma come sottolinea l'eccellente risposta di Steve Jessop e altri, la risposta a quest'ultima domanda è piuttosto lunga e discutibile. Sfortunatamente, quando si tratta dello standard C ++, è spesso quasi impossibile fornire una spiegazione breve e autonoma sul perché lo standard imponga qualcosa; questo vale anche per quest'ultima domanda.


11

Il xè nascosto durante l'eredità. Puoi scoprire tramite:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};

25
Questa risposta non spiega perché è nascosta.
Jamesdlin,
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.