Salta al contenuto
Note per Studenti Esercizio - Template, specializzazione e deduzione degli argomenti (quarto appello 2021-22, teoria)

Esercizio - Template, specializzazione e deduzione degli argomenti (quarto appello 2021-22, teoria)

Esame
In questa pagina 4

Domanda aperta 2 del quarto appello 2021-22 (12 settembre 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →. La versione sulle classi template è nel quarto appello 2022-23: Esercizio - Template di classe (quarto appello 2022-23, teoria).

Testo. Descrivere il concetto di template in C++ e la notazione utilizzata per definire una funzione template e per specializzarla per un determinato tipo. Riportare inoltre i principali vantaggi e svantaggi di questo meccanismo. Descrivere il concetto di deduzione degli argomenti del template operata in fase di compilazione.

Il concetto

Un template permette di usare un tipo come parametro (o anche un intero noto in compilazione) per una funzione o per una classe: si scrive il codice una volta sola e il compilatore genera la versione per ogni tipo per cui il template è usato (specializzazione o instantiation). La generazione avviene a tempo di compilazione. È il meccanismo del polimorfismo statico e della programmazione generica.

Notazione

Funzione template: si premette template <typename T> (equivalente a template <class T>: typename è più chiaro, class più corto) alla definizione:

cpp
template <typename T>
T myMax(T x, T y)
{
    return (x > y) ? x : y;
}

Si può indicare il tipo (myMax<int>(i, j)) o lasciarlo dedurre (myMax(i, j)).

Specializzazione esplicita per un tipo: si scrive template <> (lista vuota) e si indica il tipo dopo il nome della funzione, fornendo un'altra implementazione:

cpp
template <>
const char* myMax<const char*>(const char* x, const char* y)
{
    return (strcmp(x, y) > 0) ? x : y;       // la versione generica confronterebbe gli indirizzi
}
tmpl_funzione.cppcpp
#include <cstring>
#include <iostream>
#include <string>
using namespace std;

template <typename T>
T myMax(T x, T y)                    // T e' un tipo qualunque purché supporti l'operatore >
{
    return (x > y) ? x : y;
}

// specializzazione ESPLICITA per const char*: il confronto generico (>) confronterebbe gli indirizzi
template <>
const char* myMax<const char*>(const char* x, const char* y)
{
    return (strcmp(x, y) > 0) ? x : y;
}

struct Punto { int x, y; };          // NON ha operator>

int main()
{
    int i = 3, j = 5;
    cout << myMax<int>(i, j) << '\n';              // T indicato esplicitamente
    cout << myMax(i, j) << '\n';                   // T = int, DEDOTTO dagli argomenti
    cout << myMax(2.5, -1.0) << '\n';              // T = double
    cout << myMax(string("pesca"), string("mela")) << '\n';   // T = string
    cout << myMax("pesca", "mela") << '\n';        // T = const char*: usa la specializzazione
    // myMax(1, 2.5);                              // errore: T sarebbe int e double insieme
    // myMax(Punto{1, 2}, Punto{3, 4});            // errore: Punto non ha operator>
    return 0;
}
5
5
2.5
pesca
pesca

Nel programma myMax("pesca", "mela") usa la specializzazione (T = const char*, perché i literal decadono a puntatore) e restituisce pesca; la chiamata myMax(1, 2.5) non compila per un conflitto di tipi.

Vantaggi e svantaggi

vantaggi svantaggi
codice generico scritto una volta errori di compilazione lunghi e oscuri: il compilatore controlla sia il template sia l'argomento (invalid operands to binary expression ('Punto' and 'Punto'), con la riga dell'istanza myMax<Punto> solo in una note)
nessun costo a run-time: il codice è per il tipo esatto, inlinabile, ottimizzabile (a differenza del polimorfismo dinamico) più codice generato (una copia per tipo), compilazione più lenta, eseguibili più grandi
i controlli di tipo restano in compilazione poca separazione fra dichiarazione e definizione: le definizioni vanno negli header (nei .hpp inclusi in coda al .h)
funziona anche con tipi built-in e con interi come parametri (array<T, N>) requisiti impliciti su T (qui operator>): non scritti nell'interfaccia, si scoprono con l'errore

Deduzione degli argomenti

Il compilatore deduce gli argomenti template dagli argomenti della chiamata, a tempo di compilazione, senza che il programmatore li scriva. Con myMax(i, j), dove i e j sono int, ricava T = int: equivale a myMax<int>(i, j). Con parametri come array<T, N>&, ricava anche il valore intero N:

cpp
template<class T, int N>
void fill(array<T, N>& b, const T& val) { for (int i = 0; i < N; ++i) b[i] = val; }
fill(buf, 'x');      // T = char e N = 1024, dedotti da buf: equivale a fill<char, 1024>(buf, 'x')

Regole utili: la deduzione usa i tipi degli argomenti; se lo stesso T appare con due tipi diversi (myMax(1, 2.5)) c'è conflitto e il template è scartato (deduced conflicting types for parameter 'T' ('int' vs. 'double')); i parametri che non compaiono fra gli argomenti non si possono dedurre e vanno indicati (vector<int> v;); gli argomenti di classe template (vector<double>) si indicano sempre fra <> (da C++17 a volte sono deducibili dal costruttore).

Versione ripasso

  • Template = tipo (o intero) come parametro di funzione o classe; il compilatore genera il codice per ogni tipo usato (specializzazione/instantiation) a tempo di compilazione; polimorfismo statico.
  • Notazione: template <typename T> (≡\equiv template <class T>) davanti alla definizione; template <typename T> T myMax(T x, T y) { return (x > y) ? x : y; }. Specializzazione: template <> const char* myMax<const char*>(const char* x, const char* y) { return strcmp(x, y) > 0 ? x : y; }.
vantaggi svantaggi
codice generico, scritto una volta errori lunghi e oscuri
nessun costo a run-time più codice generato, compilazione più lenta
controlli di tipo in compilazione definizioni negli header (.h + .hpp)
anche tipi built-in e interi (array<T, N>) requisiti impliciti su T (>)
  • Deduzione: il compilatore ricava gli argomenti template dagli argomenti della chiamata: myMax(i, j) ⇒\Rightarrow T = int (≡\equiv myMax<int>(i, j)); fill(buf, 'x') ⇒\Rightarrow T = char, N = 1024. Tipi diversi per lo stesso T (myMax(1, 2.5)) = conflitto (deduced conflicting types for parameter 'T' ('int' vs. 'double')); parametri non presenti negli argomenti vanno indicati.
  • Prova: myMax<int>(3, 5) e myMax(3, 5) →\to 5; myMax(2.5, -1.0) →\to 2.5; stringhe →\to pesca; const char* con la specializzazione →\to pesca.
  • Errori tipici: definire un template in un .cpp (errore del linker); argomenti di tipi diversi per lo stesso T; dimenticare template <> nella specializzazione; credere che i template siano a run-time.

Esercizi su questo argomento

Teoria collegata