Perché questa funzione modello non si comporta come previsto?


23

Stavo leggendo le funzioni del modello e mi sono confuso con questo problema:

#include <iostream>

void f(int) {
    std::cout << "f(int)\n";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << "  ";
    f(val);
}

void f(double) {
    std::cout << "f(double)\n";
}

template void g<double>(double);

int main() {
    f(1.0); // f(double)
    f(1);   // f(int)
    g(1.0); // d  f(int), this is surprising
    g(1);   // i  f(int)
}

I risultati sono gli stessi se non scrivo template void g<double>(double);.

Penso che g<double>dovrebbe essere istanziato dopo f(double), e quindi la chiamata a fin gdovrebbe chiamare f(double). Sorprendentemente, si chiama ancora f(int)in g<double>. Qualcuno può aiutarmi a capire questo?


Dopo aver letto le risposte, ho capito qual è la mia confusione.

Ecco un esempio aggiornato. È per lo più invariato tranne per il fatto che ho aggiunto una specializzazione per g<double>:

#include <iostream>

void f(int){cout << "f(int)" << endl;}

template<typename T>
void g(T val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

void f(double){cout << "f(double)" << endl;}

//Now use user specialization to replace
//template void g<double>(double);

template<>
void g<double>(double val)
{
    cout << typeid(val).name() << "  ";
    f(val);
}

int main() {
    f(1.0); // f(double)
    f(1);  // f(int)
    g(1.0); // now d  f(double)
    g(1);  // i  f(int)
}

Con la specializzazione dell'utente, g(1.0)si comporta come mi aspettavo.

Il compilatore non dovrebbe fare automaticamente questa stessa istanza g<double>nello stesso posto (o anche dopo main(), come descritto nella sezione 26.3.3 di The C ++ Programming Language , 4th edition)?


3
L'ultima chiamata mi g(1)i f(int). Hai scritto d f(double). Era un errore di battitura?
HTNW,

sì. scusa. aggiornato
Zhongqi Cheng,

Il principio di base del modello è di supportare l'uso delle operazioni sui tipi di utente, evitando comunque il dirottamento delle chiamate interne alla libreria da parte dei simboli dichiarati dall'utente. Il che è un compromesso impossibile, poiché non esistono contratti "concettuali" per i modelli ed è troppo tardi per introdurre tali "contratti" solidi.
curiousguy,

Risposte:


12

Il nome fè un nome dipendente (dipende Tdall'argomento val) e verrà risolto in due passaggi :

  1. La ricerca non ADL esamina le dichiarazioni di funzione ... che sono visibili dal contesto di definizione del modello .
  2. ADL esamina le dichiarazioni di funzione ... che sono visibili dal contesto di definizione del modello o dal contesto di istanza del modello .

void f(double)non è visibile dal contesto di definizione del modello e ADL non lo troverà, perché

Per argomenti di tipo fondamentale, l'insieme associato di spazi dei nomi e classi è vuoto


Possiamo leggermente modificare il tuo esempio:

struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}

Ora ADL troverà void f(Double)nel secondo passaggio e l'output sarà 6Double f(Double). Possiamo disabilitare ADL scrivendo (f)(val)(o ::f(val)) invece di f(val). Quindi l'output sarà 6Double f(Int), in accordo con il tuo esempio.


Grazie mille. Mi chiedo dove sia l'istanza per g <double> nel codice. È appena prima di main (). In tal caso, la definizione g <double> non dovrebbe essere in grado di vedere sia f (int) che f (double), e infine scegliere f (double)?
Zhongqi Cheng,

@ZhongqiCheng Al passaggio 1 verrà considerato solo il contesto di definizione del modello e da quel contesto void f(double)non è visibile - questo contesto termina prima della sua dichiarazione. Al passaggio 2 ADL non troverà nulla, quindi il contesto di istanza del modello non svolge alcun ruolo qui.
Evg

@ZhongqiCheng, nella tua modifica hai introdotto una definizione dopo void f(double), quindi questa funzione è visibile da essa. Ora fnon è un nome dipendente. Se ci fosse una corrispondenza migliore per f(val);dichiarata dopo la definizione di g<double>, anche questa non verrà trovata. L'unico modo per "guardare avanti" è ADL (o qualche vecchio compilatore che non implementa correttamente la ricerca in due fasi).
Evg

Ecco la mia comprensione della tua risposta. Dovrei supporre che i modelli di funzione (g <int> e g <doppia>) siano istanziati subito dopo la definizione del modello. Pertanto non vedrà f (doppio). È corretto. Grazie mille.
Zhongqi Cheng,

@ZhongqiCheng, istanziato poco prima main(). Non vedranno f(double), perché quando si verifica l'istanza è troppo tardi: la prima fase della ricerca è già stata eseguita e non ha trovato alcun f(double).
Evg

6

Il problema f(double)non è stato dichiarato nel punto in cui lo si chiama; se sposti la sua dichiarazione davanti a template g, verrà chiamata.

Modifica: Perché si dovrebbe usare l'istanza manuale?

(Parlerò solo di modelli di funzione, argomentazioni analoghe valgono anche per i modelli di classe.) L'uso principale è ridurre i tempi di compilazione e / o nascondere il codice del modello agli utenti.

I programmi C ++ sono integrati in file binari in 2 passaggi: compilazione e collegamento. Perché la compilazione di una chiamata di funzione abbia esito positivo, è necessaria solo l'intestazione della funzione. Perché il collegamento abbia esito positivo, è necessario un file oggetto contenente il corpo compilato della funzione.

Ora, quando il compilatore vede una chiamata di una funzione basata su modelli , cosa fa dipende dal fatto che conosca il corpo del modello o solo l'intestazione. Se vede solo l'intestazione fa la stessa cosa che se la funzione non fosse stata modificata: inserisce le informazioni sulla chiamata per il linker nel file oggetto. Ma se vede anche il corpo del modello fa anche un'altra cosa: crea un'istanza corretta del corpo, compila questo corpo e lo inserisce anche nel file oggetto.

Se più file di origine chiamano la stessa istanza della funzione modello, ciascuno dei loro file oggetto conterrà una versione compilata dell'istanza della funzione. (Linker lo sa e risolve tutte le chiamate in una singola funzione compilata, quindi ce ne sarà solo una nell'ultimo binario del programma / libreria.) Tuttavia, al fine di compilare ciascuno dei file sorgente, la funzione ha dovuto essere istanziata e compilato, che ha richiesto tempo.

È sufficiente che il linker faccia il suo lavoro se il corpo della funzione si trova in un file oggetto. Creare un'istanza manuale del modello in un file di origine è un modo per fare in modo che il compilatore inserisca il corpo della funzione nel file oggetto del file di origine in questione. (È un po 'come se la funzione fosse chiamata, ma l'istanza è scritta in un posto in cui la chiamata di funzione non sarebbe valida.) Al termine, tutti i file che chiamano la tua funzione possono essere compilati conoscendo solo l'intestazione della funzione, quindi risparmiando tempo ci vorrebbe per istanziare e compilare il corpo della funzione con ciascuna delle chiamate.

La seconda ragione (nascondere l'implementazione) potrebbe avere senso ora. Se l'autore di una biblioteca desidera che gli utenti della sua funzione modello possano utilizzare la funzione, di solito fornisce loro il codice del modello, in modo che possano compilarlo da soli. Se voleva mantenere segreto il codice sorgente del modello, potrebbe creare un'istanza manuale del modello nel codice che utilizza per creare la libreria e fornire agli utenti la versione dell'oggetto così ottenuta invece del sorgente.

Questo ha senso?


Ti sarei grato se riuscissi a spiegare la differenza tra l'istanza presentata nel primo codice dell'autore e la specializzazione nel secondo codice dell'autore dopo la modifica. Ho letto molte volte il sito di cppreference su specializzazione e istanziazione e libri, ma non ho capito. Grazie
Dev

@Dev: specifica un po 'di più la tua domanda, non sono sicuro di cosa rispondere. Fondamentalmente in questo caso la differenza è che quando è presente la specializzazione il compilatore la utilizza, mentre quando non è presente, il compilatore prende il modello, ne genera un'istanza e usa questa istanza generata. Nel codice sopra sia la specializzazione che l'istanza del modello portano allo stesso codice.
AshleyWilkes,

La mia domanda riguarda esattamente quella parte del codice: "template void g <double> (double);" È noto come istanza nel modello di programmazione, se lo sai. La specializzazione è leggermente diversa, poiché è dichiarata come nel secondo codice l'autore ha inviato "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Potresti spiegarmi la differenza?
Dev

@Dev Ho già provato a farlo: il compilatore usa una specializzazione se può; se non riesce a vedere la specializzazione (ad es. perché non ce ne sono) il compilatore crea un'istanza del modello e usa quell'istanza. Nel codice sopra sia il modello che la specializzazione portano allo stesso risultato, quindi l'unica differenza è in ciò che il compilatore fa per ottenere quel risultato. In altri casi la specializzazione potrebbe contenere qualsiasi implementazione, non deve avere nulla in comune con il modello (ma per l'intestazione del metodo). Più chiaro?
AshleyWilkes,

1
La template void g<double>(double);cosiddetta istanziazione manuale (notare la templatesenza parentesi angolari, questa è una caratteristica distintiva della sintassi); che dice al compilatore di creare un'istanza del metodo. Qui ha scarso effetto, se non fosse presente, il compilatore genererebbe l'istanza nel luogo in cui viene chiamata l'istanza. L'istanza manuale viene utilizzata raramente, dico perché potresti voler usarla dopo aver confermato che la cosa è ora più chiara :-)
AshleyWilkes,
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.