Risposta breve: per creare x
un 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 int
allora il codice sopra è mal formato e deve essere diagnosticato, proprio come sarebbe se foo
non 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. A
qui 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::A
sarebbe 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 T
in 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 typename
per specificare che sono tipi o utilizzato in determinati contesti non ambigui. Ad esempio template <typename T> struct Foo : T::A {};
, T::A
viene 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::string
sarebbe 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 Bar
definito in precedenza Foo
, e x
non è un membro in quella definizione, qualcuno potrebbe in seguito definire una specializzazione Bar
per 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 Bar
avesse x
, potrebbero definire una specializzazione senza di essa e il tuo modello cercherebbe un x
ritorno 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 A
dipendenti:
using Bar<T>::A;
nella classe - A
ora 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é typename
non è 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::string
potrebbe 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.