c++ - Dove e perché devo inserire le parole chiave "template" e "typename"?




templates c++-faq (4)

PREFAZIONE

Questo post è pensato per essere un'alternativa di facile lettura al post di litb .

Lo scopo di fondo è lo stesso; una spiegazione a "Quando?" e perché?" typename e template devono essere applicati.

Qual è lo scopo di typename e template ?

typename e template sono utilizzabili in circostanze diverse da quando si dichiara un modello.

Ci sono certi contesti in C ++ in cui al compilatore deve essere esplicitamente detto come trattare un nome, e tutti questi contesti hanno una cosa in comune; dipendono da almeno un parametro di modello .

Ci riferiamo a tali nomi, dove può esserci un'ambiguità nell'interpretazione, come; " nomi dipendenti ".

Questo post offrirà una spiegazione della relazione tra nomi dipendenti e le due parole chiave.

UN SNIPPET DICE PIÙ DI 1000 PAROLE

Cerca di spiegare cosa sta succedendo nel seguente modello di funzione , a te stesso, a un amico o forse al tuo gatto; cosa sta succedendo nella frase contrassegnata ( A )?

template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }


Potrebbe non essere così facile come si pensa, in particolare il risultato della valutazione ( A ) dipende pesantemente dalla definizione del tipo passato come parametro-modello T

Differenti T possono cambiare drasticamente la semantica coinvolta.

struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();


I due diversi scenari :

  • Se istanziamo il modello di funzione con il tipo X , come in ( C ), avremo una dichiarazione di un puntatore a int chiamato x , ma;

  • se istanziamo il modello con il tipo Y , come in ( D ), invece ( A ) sarebbe costituito da un'espressione che calcola il prodotto di 123 moltiplicato con una variabile già dichiarata x .


LA LOGICA

Lo standard C ++ si preoccupa della nostra sicurezza e del nostro benessere, almeno in questo caso.

Per evitare che un'implementazione risenta potenzialmente di brutte sorprese, lo Standard impone di risolvere l'ambiguità di un nome-dipendente dichiarando esplicitamente l'intento ovunque vorremmo trattare il nome come un nome-tipo o un modello- id .

Se non viene indicato nulla, il nome dipendente verrà considerato come una variabile o una funzione.


COME MANEGGIARE I NOME DIPENDENTI ?

Se si trattava di un film di Hollywood, i nomi dipendenti sarebbero la malattia che si diffonde attraverso il contatto con il corpo, colpisce immediatamente il suo ospite per renderlo confuso. Confusione che potrebbe, eventualmente, portare a un programma perso-, erhm.

Un nome dipendente è un nome che direttamente o indirettamente dipende da un parametro del modello .

template<class T> void g_tmpl () {
   SomeTrait<T>::type                   foo; // (E), ill-formed
   SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
   foo.data<int> ();                         // (G), ill-formed    
}

Abbiamo quattro nomi dipendenti nello snippet sopra riportato:

  • E )
    • "type" dipende SomeTrait<T> di SomeTrait<T> , che include T , e;
  • F )
    • "NestedTrait" , che è un id-template , dipende da SomeTrait<T> , e;
    • "type" alla fine di ( F ) dipende da NestedTrait , che dipende da SomeTrait<T> , e;
  • G )
    • "data" , che assomiglia a un modello di funzione membro , è indirettamente un nome dipendente poiché il tipo di pippo dipende SomeTrait<T> di SomeTrait<T> .

Nessuna delle affermazioni ( E ), ( F ) o ( G ) è valida se il compilatore interpretasse i nomi dipendenti come variabili / funzioni (che come affermato in precedenza è ciò che accade se non diciamo esplicitamente altrimenti).

LA SOLUZIONE

Per fare in modo che g_tmpl abbia una definizione valida dobbiamo dire esplicitamente al compilatore che ci aspettiamo un type in ( E ), un template-id e un type in ( F ), e un template-id in ( G ).

template<class T> void g_tmpl () {
   typename SomeTrait<T>::type foo;                            // (G), legal
   typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
   foo.template data<int> ();                                  // (I), legal
}

Ogni volta che un nome indica un tipo, tutti i nomi coinvolti devono essere nomi di tipo o spazi dei nomi , con questo in mente è abbastanza facile vedere che applichiamo typename all'inizio del nostro nome completo.

template , tuttavia, è diverso in questo senso, poiché non c'è modo di giungere a una conclusione come; "oh, questo è un modello, che quest'altra cosa deve essere anche un modello" . Ciò significa che applichiamo il template direttamente davanti a qualsiasi nome che vorremmo trattare come tale.


POSSO SOLO APPARE LE PAROLE CHIAVE DAVANTI A QUALSIASI NOME?

" Posso semplicemente attaccare typename e template davanti a qualsiasi nome? Non voglio preoccuparmi del contesto in cui appaiono ... " - Some C++ Developer

Le regole dello Standard affermano che è possibile applicare le parole chiave purché si tratti di un nome qualificato ( K ), ma se il nome non è qualificato l'applicazione è mal formata ( L ).

namespace N {
  template<class T>
  struct X { };
}

         N::         X<int> a; // ...  legal
typename N::template X<int> b; // (K), legal
typename template    X<int> c; // (L), ill-formed

Nota : l'applicazione di typename o template in un contesto in cui non è richiesto non è considerata una buona pratica; solo perché puoi fare qualcosa, non significa che dovresti.


Inoltre ci sono contesti in cui typename e template sono esplicitamente non consentiti:

  • Quando si specificano le basi di cui una classe eredita

    Ogni nome scritto nella lista degli specificatori di base di una classe derivata è già trattato come un nome-tipo , specificando esplicitamente che typename è sia mal formato che ridondante.

                       // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };
    


  • Quando l' id-template è quello a cui si fa riferimento nella direttiva using di una classe derivata

     struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };
    

Nei template, dove e perché devo inserire typename e template sui nomi dipendenti? Quali sono esattamente i nomi dipendenti comunque? Ho il codice seguente:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

Il problema che ho è nella riga typedef Tail::inUnion<U> dummy . Sono abbastanza certo che inUnion è un nome dipendente, e VC ++ ha perfettamente ragione a soffocarlo. So anche che dovrei essere in grado di aggiungere template da qualche parte per dire al compilatore che inUnion è un id-template. Ma dove esattamente? E dovrebbe quindi supporre che inUnion sia un modello di classe, cioè inUnion<U> nomi un tipo e non una funzione?


C ++ 11

Problema

Mentre le regole in C ++ 03 su quando hai bisogno di typename e template sono ampiamente ragionevoli, c'è un fastidioso svantaggio della sua formulazione

template<typename T>
struct A {
  typedef int result_type;

  void f() {
    // error, "this" is dependent, "template" keyword needed
    this->g<float>();

    // OK
    g<float>();

    // error, "A<T>" is dependent, "typename" keyword needed
    A<T>::result_type n1;

    // OK
    result_type n2; 
  }

  template<typename U>
  void g();
};

Come si può vedere, abbiamo bisogno della parola chiave disambiguazione anche se il compilatore riuscisse a capire perfettamente che A::result_type può essere solo int (ed è quindi un tipo), e this->g può essere solo il template membro g dichiarato in seguito (anche se A è esplicitamente specializzato da qualche parte, ciò non influenzerebbe il codice all'interno di quel modello, quindi il suo significato non può essere influenzato da una successiva specializzazione di A !).

Istanza corrente

Per migliorare la situazione, in C ++ 11 la lingua tiene traccia quando un tipo fa riferimento al modello allegato. Per saperlo, il tipo deve essere stato formato usando una certa forma di nome, che è il suo nome (in A , A<T> , ::A<T> ). Un tipo riferito a tale nome è noto per essere l' istanza corrente . Potrebbero esserci più tipi che rappresentano l'istanza corrente se il tipo da cui è formato il nome è una classe membro / nidificata (quindi, A::NestedClass e A sono entrambe istanze correnti).

Sulla base di questa nozione, la lingua dice che CurrentInstantiation::Foo , Foo e CurrentInstantiationTyped->Foo (come A *a = this; a->Foo ) sono tutti membri dell'attuale istanza se si trovano membri di una classe che è l'istanza corrente o una delle sue classi di base non dipendenti (eseguendo immediatamente la ricerca del nome).

Le parole chiave typename e template ora non sono più necessarie se il qualificatore è membro dell'istanza corrente. Un punto chiave da ricordare è che A<T> è ancora un nome dipendente dal tipo (dopo tutto T è anche dipendente dal tipo). Ma A<T>::result_type è noto per essere un tipo - il compilatore "magicamente" esaminerà questo tipo di tipi dipendenti per capirlo.

struct B {
  typedef int result_type;
};

template<typename T>
struct C { }; // could be specialized!

template<typename T>
struct D : B, C<T> {
  void f() {
    // OK, member of current instantiation!
    // A::result_type is not dependent: int
    D::result_type r1;

    // error, not a member of the current instantiation
    D::questionable_type r2;

    // OK for now - relying on C<T> to provide it
    // But not a member of the current instantiation
    typename D::questionable_type r3;        
  }
};

È impressionante, ma possiamo fare di meglio? Il linguaggio va anche oltre e richiede che un'implementazione guardi nuovamente su D::result_type durante l'istanziazione di D::f (anche se ha trovato il suo significato già al momento della definizione). Quando ora il risultato della ricerca differisce o si traduce in ambiguità, il programma è mal formato e deve essere fornita una diagnosi. Immagina cosa succede se abbiamo definito C come questo

template<>
struct C<int> {
  typedef bool result_type;
  typedef int questionable_type;
};

È richiesto un compilatore per rilevare l'errore durante l'istanziazione di D<int>::f . In questo modo ottieni il meglio dei due mondi: la ricerca "differita" ti protegge se potresti avere problemi con le classi di base dipendenti e anche la ricerca "immediata" che ti libera da typename e template .

Specializzazioni sconosciute

Nel codice di D , il nome typename D::questionable_type non è un membro dell'attuale istanza. Invece la lingua lo segna come membro di una specializzazione sconosciuta . In particolare, questo è sempre il caso quando si fa DependentTypeName::Foo o DependentTypedName->Foo e il tipo dipendente non è l'istanza corrente (nel qual caso il compilatore può rinunciare e dire "guarderemo più tardi che cos'è Foo ) o è l'istanza corrente e il nome non è stato trovato in esso o le sue classi di base non dipendenti e ci sono anche classi di base dipendenti.

Immagina cosa succede se avessimo una funzione membro h all'interno del modello di classe A sopra definito

void h() {
  typename A<T>::questionable_type x;
}

In C ++ 03, il linguaggio ha permesso di catturare questo errore perché non ci sarebbe mai stato un modo valido per istanziare A<T>::h (qualunque argomento tu dia a T ). In C ++ 11, la lingua ora ha un ulteriore controllo per dare più motivi per i compilatori di implementare questa regola. Poiché A non ha classi di base dipendenti e A non dichiara alcun tipo questionable_type , il nome A<T>::questionable_type non è un membro dell'istanza corrente un membro di una specializzazione sconosciuta. In tal caso, non dovrebbe esserci modo che quel codice possa validamente compilare al momento dell'istanziazione, quindi la lingua proibisce un nome in cui il qualificatore è l'istanza corrente per non essere né un membro di una specializzazione sconosciuta né un membro dell'istanza corrente (tuttavia , questa violazione non è ancora richiesta per essere diagnosticata).

Esempi e curiosità

Puoi provare questa conoscenza su questa risposta e vedere se le definizioni di cui sopra hanno un senso per te su un esempio del mondo reale (sono ripetute leggermente meno dettagliate in quella risposta).

Le regole del C ++ 11 rendono malformato il seguente codice C ++ 03 (che non era inteso dal comitato C ++, ma probabilmente non verrà risolto)

struct B { void f(); };
struct A : virtual B { void f(); };

template<typename T>
struct C : virtual B, T {
  void g() { this->f(); }
};

int main() { 
  C<A> c; c.g(); 
}

Questo codice C ++ 03 valido legherebbe this->f ad A::f al momento dell'istanziazione e tutto andrà bene. C ++ 11 tuttavia lo lega immediatamente a B::f e richiede un doppio controllo durante l'istanziazione, controllando che la ricerca corrisponda ancora. Tuttavia, quando si esegue l'istanziazione di C<A>::g , si applica la regola del dominio e la ricerca troverà invece A::f .


Sto ponendo l'eccellente response di JLBorges a una domanda simile testualmente da cplusplus.com, in quanto è la spiegazione più succinta che ho letto sull'argomento.

In un modello che scriviamo, ci sono due tipi di nomi che potrebbero essere usati: nomi dipendenti e nomi non dipendenti. Un nome dipendente è un nome che dipende da un parametro del modello; un nome non dipendente ha lo stesso significato indipendentemente da quali siano i parametri del template.

Per esempio:

template< typename T > void foo( T& x, std::string str, int count )
{
    // these names are looked up during the second phase
    // when foo is instantiated and the type T is known
    x.size(); // dependant name (non-type)
    T::instance_count ; // dependant name (non-type)
    typename T::iterator i ; // dependant name (type)

    // during the first phase, 
    // T::instance_count is treated as a non-type (this is the default)
    // the typename keyword specifies that T::iterator is to be treated as a type.

    // these names are looked up during the first phase
    std::string::size_type s ; // non-dependant name (type)
    std::string::npos ; // non-dependant name (non-type)
    str.empty() ; // non-dependant name (non-type)
    count ; // non-dependant name (non-type)
}

A cosa si riferisce un nome dipendente potrebbe essere qualcosa di diverso per ogni diversa istanziazione del modello. Di conseguenza, i modelli C ++ sono soggetti alla "ricerca del nome in due fasi". Quando un modello viene inizialmente analizzato (prima che venga eseguita un'istanza), il compilatore cerca i nomi non dipendenti. Quando si verifica una particolare istanziazione del modello, i parametri del modello sono noti a quel punto e il compilatore cerca i nomi dipendenti.

Durante la prima fase, il parser deve sapere se un nome dipendente è il nome di un tipo o il nome di un non-tipo. Per impostazione predefinita, si presuppone che il nome dipendente sia il nome di un non-tipo. La parola chiave typename prima di un nome dipendente specifica che si tratta del nome di un tipo.

Sommario

Utilizzare la parola chiave typename solo nelle dichiarazioni e nelle definizioni del modello, purché si disponga di un nome qualificato che faccia riferimento a un tipo e che dipenda da un parametro del modello.


Questa risposta vuole essere piuttosto corta e dolce per rispondere (in parte) alla domanda intitolata. Se vuoi una risposta con più dettagli che spieghi perché devi metterli lì, per favore vai here .

La regola generale per inserire la parola chiave typename è principalmente quando si utilizza un parametro del modello e si desidera accedere a un typedef annidato oa un alias di utilizzo, ad esempio:

template<typename T>
struct test {
    using type = T; // no typename required
    using underlying_type = typename T::type // typename required
};

Nota che questo vale anche per le meta-funzioni o per le cose che richiedono anche parametri generici per i modelli. Tuttavia, se il parametro del template fornito è di tipo esplicito, non è necessario specificare typename , ad esempio:

template<typename T>
struct test {
    // typename required
    using type = typename std::conditional<true, const T&, T&&>::type;
    // no typename required
    using integer = std::conditional<true, int, float>::type;
};

Le regole generali per l'aggiunta del qualificatore del template sono per lo più simili, tranne che in genere coinvolgono le funzioni dei membri basate su modelli (statiche o meno) di una struttura / classe che è a sua volta basata su modelli, ad esempio:

Data questa struttura e funzione:

template<typename T>
struct test {
    template<typename U>
    void get() const {
        std::cout << "get\n";
    }
};

template<typename T>
void func(const test<T>& t) {
    t.get<int>(); // error
}

Il tentativo di accedere a t.get<int>() dall'interno della funzione provocherà un errore:

main.cpp:13:11: error: expected primary-expression before 'int'
     t.get<int>();
           ^
main.cpp:13:11: error: expected ';' before 'int'

Quindi in questo contesto avresti bisogno in anticipo della parola chiave template e la chiamerai in questo modo:

t.template get<int>()

In questo modo il compilatore analizzerà questo correttamente piuttosto che t.get < int .





dependent-name