Ordinamento parziale dei modelli: perché la deduzione parziale ha successo qui

Si consideri il seguente esempio semplice (nella misura in cui tale modello è sempre):

#include  template  struct identity; template  struct identity { using type = int; }; template void bar(T, T ) { std::cout << "a\n"; } template void bar(T, typename identity::type) { std::cout << "b\n"; } int main () { bar(0, 0); } 

Sia clang che gcc stampano “a” lì. Secondo le regole in [temp.deduct.partial] e [temp.func.order], per determinare l’ordinamento parziale, dobbiamo sintetizzare alcuni tipi unici. Quindi abbiamo due tentativi di deduzione:

 +---+-------------------------------+-------------------------------------------+ | | Parameters | Arguments | +---+-------------------------------+-------------------------------------------+ | a | T, typename identity::type | UniqueA, UniqueA | | b | T, T | UniqueB, typename identity::type | +---+-------------------------------+-------------------------------------------+ 

Per la deduzione su “b”, secondo la risposta di Richard Corden , l’espressione typename identity::type è considerata come un tipo e non viene valutata. Cioè, questo sarà sintetizzato come se fosse:

 +---+-------------------------------+--------------------+ | | Parameters | Arguments | +---+-------------------------------+--------------------+ | a | T, typename identity::type | UniqueA, UniqueA | | b | T, T | UniqueB, UniqueB_2 | +---+-------------------------------+--------------------+ 

È chiaro che la deduzione su “b” fallisce. Questi sono due tipi diversi, quindi non è ansible dedurre T per entrambi.

Tuttavia, mi sembra che la detrazione su A debba fallire. Per il primo argomento, dovresti corrispondere a T == UniqueA . Il secondo argomento è un contesto non dedotto, quindi la deduzione non verrebbe UniqueA se UniqueA fosse convertibile in identity::type ? Quest’ultimo è un errore di sostituzione, quindi non vedo come questa deduzione potrebbe avere successo.

Come e perché gcc e clang preferiscono il sovraccarico “a” in questo scenario?

Come discusso nei commenti, credo che ci siano diversi aspetti dell’algoritmo di ordinamento parziale del modello di funzione che non sono chiari o non specificati affatto nello standard, e questo si vede nel tuo esempio.

Per rendere le cose ancora più interessanti, MSVC (ho testato 12 e 14) respinge la chiamata come ambigua. Non penso che ci sia qualcosa nello standard per dimostrare in modo definitivo quale compilatore ha ragione, ma penso che potrei avere un indizio su da dove viene la differenza; c’è una nota a riguardo qui sotto.

La tua domanda (e questa ) mi ha sfidato a fare qualche altra indagine su come funzionano le cose. Ho deciso di scrivere questa risposta non perché la consideri autorevole, ma piuttosto di organizzare le informazioni che ho trovato in un posto (non si adatta ai commenti). Spero che sarà utile.


Innanzitutto, la proposta di risoluzione per il numero 1391 . Ne abbiamo discusso ampiamente nei commenti e nelle chat. Penso che, mentre fornisce alcuni chiarimenti, introduce anche alcuni problemi. Cambia [14.8.2.4p4] in (nuovo testo in grassetto):

Ogni tipo nominato sopra dal modello di parametro e il tipo corrispondente dal modello di argomento vengono utilizzati come tipi di P e A Se una particolare P non contiene parametri modello che partecipano alla deduzione degli argomenti del modello, tale P non viene utilizzata per determinare l’ordine.

Non è una buona idea secondo me, per diversi motivi:

  • Se P non è dipendente, non contiene alcun parametro del modello, quindi non contiene nessuno che partecipi alla deduzione argomento, il che farebbe applicare la dichiarazione in grassetto. Tuttavia, ciò renderebbe il template f(T, int) e template f(T, U) non ordinato, il che non ha senso. Questa è probabilmente una questione di interpretazione della dicitura, ma potrebbe causare confusione.
  • Mette a disagio la nozione di usato per determinare l’ordinamento , che influisce su [14.8.2.4p11]. Questo rende il template void f(T) e template void f(typename A::a) non ordinato (la deduzione ha successo dal primo al secondo, perché T non è usato in un tipo usato per ordinando secondo la nuova regola, quindi può rimanere senza valore). Attualmente, tutti i compilatori che ho testato riportano il secondo come più specializzato.
  • Renderebbe #2 più specializzato del #1 nel seguente esempio:

     #include  template struct A { using a = T; }; struct D { }; template struct B { B() = default; B(D) { } }; template struct C { C() = default; C(D) { } }; template void f(T, B) { std::cout << "#1\n"; } // #1 template void f(T, C::a>) { std::cout << "#2\n"; } // #2 int main() { f(1, D()); } 

    Il secondo parametro di #2 non viene utilizzato per l’ordinamento parziale, quindi la deduzione ha esito positivo da #1 a #2 ma non viceversa. Al momento, la chiamata è ambigua e dovrebbe rimanere discutibile.


Dopo aver esaminato l’implementazione dell’algoritmo di ordinamento parziale di Clang, ecco come penso che il testo standard possa essere modificato per riflettere ciò che effettivamente accade.

Lascia [p4] così com’è e aggiungi quanto segue tra [p8] e [p9]:

Per una coppia P / A :

  • Se P è non dipendente, la deduzione è considerata valida se e solo se P e A sono dello stesso tipo.
  • La sostituzione dei parametri del modello dedotti nei contesti non dedotti che compaiono in P non viene eseguita e non influisce sul risultato del processo di deduzione.
  • Se i valori degli argomenti del template vengono dedotti con successo per tutti i parametri del template di P tranne quelli che appaiono solo in contesti non dedotti, allora la deduzione è considerata riuscita (anche se alcuni parametri utilizzati in P rimangono senza valore alla fine del processo di deduzione per quella particolare coppia P / A ).

Gli appunti:

  • Informazioni sul secondo punto elenco: [14.8.2.5p1] parla della ricerca di valori di argomenti modello che renderanno P , dopo la sostituzione dei valori dedotti (chiamiamolo A dedotto), compatibili con A Ciò potrebbe causare confusione su ciò che accade effettivamente durante l’ordinamento parziale; non c’è sostituzione in corso.
  • MSVC non sembra implementare il terzo punto in alcuni casi. Vedi la prossima sezione per i dettagli.
  • Il secondo e il terzo punto elenco puntano a coprire anche casi in cui P ha forms come A , che non sono trattate nel testo 1391.

Cambia il corrente [p10] in:

Il modello di funzione F è almeno tanto specializzato quanto il modello di funzione G se e solo se:

  • per ogni coppia di tipi utilizzata per determinare l’ordine, il tipo da F è almeno specializzato quanto il tipo da G , e,
  • quando si esegue la deduzione utilizzando la F trasformata come modello di argomento e G come modello di parametro, dopo aver dedotto tutte le coppie di tipi, tutti i parametri di modello utilizzati nei tipi da G utilizzati per determinare l’ordine hanno valori e quei valori sono coerenti su tutte le coppie di tipi.

F è più specializzato di G se F è specializzato almeno quanto G e G non è specializzato almeno quanto F

Rendi nota tutta la corrente [p11].

(Anche la nota aggiunta dalla risoluzione del 1391 a [14.8.2.5p4] deve essere regolata: va bene per [14.8.2.1], ma non per [14.8.2.4].)


Per MSVC, in alcuni casi, sembra che tutti i parametri del modello in P necessitino di ricevere valori durante la detrazione per quella specifica coppia P / A in modo che la detrazione abbia successo da A a P Penso che questo potrebbe essere ciò che causa la divergenza nell’implementazione nel tuo esempio e in altri, ma ho visto almeno un caso in cui non sembra che quanto sopra sia applicabile, quindi non sono sicuro di cosa credere.

Un altro esempio in cui sembra valere l’affermazione precedente: cambiare template void bar(T, T) a template void bar(T, U) nel tuo esempio swap results around: the call is ambiguo in Clang e GCC, ma risolve in b in MSVC.

Un esempio in cui non lo fa:

 #include  template struct A { using a = T; }; template struct B { }; template void f(B) { std::cout << "#1\n"; } template void f(B::a>) { std::cout << "#2\n"; } int main() { f(B()); } 

Ciò seleziona #2 in Clang e GCC, come previsto, ma MSVC rifiuta la chiamata come ambigua; non ho idea del perché.


L’algoritmo di ordinamento parziale come descritto nello standard parla di sintetizzare un modello univoco di tipo, valore o class per generare gli argomenti. Clang lo gestisce … non sintetizzando nulla. Usa solo le forms originali dei tipi dipendenti (come dichiarato) e li combina in entrambe le direzioni. Questo ha senso, poiché la sostituzione dei tipi sintetizzati non aggiunge nuove informazioni. Non può cambiare le forms dei tipi A , poiché generalmente non c’è modo di dire a quali tipi di calcestruzzo le forms sostituite potrebbero risolversi. I tipi sintetizzati sono sconosciuti, il che li rende simili ai parametri del modello.

Quando si incontra un P che è un contesto non dedotto, l’algoritmo di deduzione degli argomenti del modello di Clang semplicemente lo ignora, restituendo “successo” per quel particolare passaggio. Ciò accade non solo durante l’ordinamento parziale, ma per tutti i tipi di detrazioni, e non solo al livello più alto in un elenco di parametri di funzione, ma ricorsivamente ogni volta che si incontra un contesto non dedotto sotto forma di un tipo composto. Per qualche ragione, ho scoperto che la prima volta che l’ho visto è stato sorprendente. Pensandoci, ovviamente ha senso ed è conforms allo standard ( […] non partecipa alla deduzione del tipo […] in [14.8.2.5p4]).

Questo è coerente con i commenti di Richard Corden alla sua risposta , ma ho dovuto vedere il codice del compilatore per capire tutte le implicazioni (non per colpa della sua risposta, ma piuttosto del mio – programmatore che pensa in codice e tutto il resto).

Ho incluso alcune ulteriori informazioni sull’implementazione di Clang in questa risposta .

Credo che la chiave sia con la seguente dichiarazione:

Il secondo argomento è un contesto non dedotto, quindi la deduzione non sarebbe se Ifa Unique fosse convertibile in identity :: type?

La detrazione di tipo non esegue il controllo di “conversioni”. Questi controlli avvengono utilizzando gli argomenti reali espliciti e dedotti come parte della risoluzione del sovraccarico.

Questo è il mio sumrio dei passaggi che vengono intrapresi per selezionare il modello di funzione da chiamare (tutti i riferimenti presi da N3937, ~ C ++ ’14):

  1. Gli argomenti espliciti vengono sostituiti e il tipo di funzione risultante ha verificato che sia valido. (14.8.2 / 2)
  2. La deduzione di tipo viene eseguita e gli argomenti dedotti risultanti vengono sostituiti. Anche in questo caso il tipo risultante deve essere valido. (14.8.2 / 5)
  3. I modelli di funzione che hanno avuto successo nei passaggi 1 e 2 sono specializzati e inclusi nel set di sovraccarico per la risoluzione del sovraccarico. (14.8.3 / 1)
  4. Le sequenze di conversione sono confrontate dalla risoluzione di sovraccarico. (13.3.3)
  5. Se le sequenze di conversione di due specializzazioni di funzione non sono “migliori”, l’algoritmo di ordinamento parziale viene utilizzato per trovare il modello di funzione più specializzato. (13.3.3)
  6. L’algoritmo di ordinamento parziale verifica solo che la deduzione di tipo abbia esito positivo. (14.5.6.2/2)

Il compilatore sa già dal punto 4 che entrambe le specializzazioni possono essere chiamate quando vengono utilizzati gli argomenti reali. I passaggi 5 e 6 vengono utilizzati per determinare quale delle funzioni è più specializzata.