Informatica 2 Liceo Scientifico Scienze Applicate/Template

Indice del libro

I Template modifica

I Template sono utilizzati in C++ nella metaprogrammazione, ovvero nella scrittura di codice generico e modificabile dal programma stesso. L'aspetto più importante è appunto la scrittura di codice generico. Normalmente quando si scrive una funzione, a meno di overload, essa ha un unico elenco di parametri e sicuramente un unico tipo di ritorno. I Template permettono di creare un'unica funzione che poi il compilatore genera per ogni tipo per cui viene utilizzata. Ad esempio, creare una funzione di somma richiede un overload per ogni tipo (int, long, long long, float, ...). Con i Template invece è possibile ridurre il tutto in poche linee di codice:

template <typename T> // dichiaro il tipo T, che è assegnabile come argomento
double Sum(T&& arg1, T&& arg2) // utilizzo il tipo T come tipo dei parametri della funzione
{
      return arg1 + arg2;
} // fine dello scope del tipo T

La funzione è poi chiamabile da altre (come il main())

int main()
{
	cout << Sum<double>(1.1, 9.78) << endl; // chiamo la funzione con tipo double
}

La chiamata non risulta quindi molto diversa rispetto alle chiamate a normali funzioni. Ora è quindi possibile sommare qualsiasi tipo. Non sono però assenti limitazioni. Innanzitutto non è possibile chiamarla per due tipi diversi, quali double ed int ad esempio, e come seconda cosa il tipo di ritorno è sempre un double, anche se la somma è tra due unsigned long long, il che è un problema. La soluzione a ciò è:

template <typename T, typename U>
decltype(std::declval<T>() + std::declval<U>()) Sum(T&& arg1, U&& arg2)
{
      return arg1 + arg2;
}

Si possono notare ora due tipi "ignoti": T ed U. Il tipo di ritorno è strano! decltype(std::declval<T>() + std::declval‹U›()) ritorna il tipo della somma di due oggetti di T e di U. È come scrivere TipoDellaSommaDiTeU. decltype() ritorna il tipo a tempo di compilazione dell'espressione contenuta mentre std::declval<>(), anch'esso un Template, ritorna un oggetto del tipo contenuto come parametro Template (è utile quando bisogna creare un oggetto di un tipo se non si conosce il suo costruttore). E così possibile ottenere il tipo corretto da una operazione di somma. La chiamata è sempre simile, ma questa volta gli argomenti del Template sono due:

int main()
{
	cout << Sum<int, double>(1, 9.78) << endl; // otteniamo un double
}

Come nelle normali funzioni, è possibile indicare argomenti facoltativi che vanno posti per ultimi. La sintassi è molto simile:

template <typename T, typename U = const char* const &> // U va posto dopo T
...

Infine bisogna precisare che gli argomenti Template non sono solo tipi, è possibile infatti passare anche oggetti, che devono essere noti a tempo di compilazione (quindi solo oggetti globali) o rvalue o interi o enumerazioni. Inoltre i Template si estendono alle strutture e alle classi

template <class container_type = int, size_t s = 10>
class Array
{
private:
    container_type* ptr;
    size_t size = s;
...
};

Inoltre typename e class sono sinonimi se usati per argomenti template, quindi la precedente funzione Sum equivale a questa:

template <class T, class U>
decltype(std::declval<T>() + std::declval<U>()) Sum(T&& arg1, U&& arg2)
{
      return arg1 + arg2;
}

typename è utilizzato se i Template vengono introdotti prima delle classi e si vuole nascondere l’esistenza della keyword class.

Template Argument Deduction modifica

Quando è possibile dedurre il tipo è possibile evitare le chiamate descritte sopra e semplificarle:

int main()
{
	cout << Sum(1, 9.78) << endl; 
}

Che equivale a:

int main()
{
	cout << Sum<int, double>(1, 9.78) << endl; 
}

Il compilatore è in grado di dedurre i tipi automaticamente, senza specificarli. Alcuni Template richiedono però un tipo obbligatoriamente, come std::function<> ma anche std::declval<>(). Questo accade quando ad esempio il tipo (o tipi) ignoto è il tipo di ritorno e non vi sono parametri che lo facciano dedurre.

Specializzazione di un Template modifica

Immaginiamo di avere una classe Array tipo questa

template <class T>
class Array
{
    ...
};

Quindi possiamo dichiarare un Array per ogni tipo, ma le funzioni disponibili saranno sempre le stesse. Ora immaginiamo di voler un operator!() per un Array<bool> che semplicemente crea il complementare dell'Array. Beh, non potremmo normalmente perché dovremo implementare l'operator!() per tutti gli Array e non è quello che vogliamo. Possiamo però specializzare la classe per <bool> in questo modo:

template <>
class Array<bool>
{
public:
    Array operator!() noexcept
    {
        Array<bool> temp;
        ...
        return temp;
    }
};

Teniamo presente però che nella specializzazione dobbiamo riscrivere l'intera classe. Non vi è alcuna derivazione dalle altre.

La specializzazione entra in gioco quando si parla di metaprogrammazione. Prima di C++11, dove non era presente il specifier constexpr, per realizzare certe funzioni che dovevano essere valutate a tempo di compilazione si doveva utilizzare i Template. Ad esempio proviamo a calcolare il fattoriale di un numero con i Template:

template <unsigned long long v>
struct Fattoriale
{
    enum
    {
        value = Fattoriale<v - 1>::value * v
    };
};

template <>
struct Fattoriale<0>
{
    enum
    {
        value = 1
    };
};

E si utilizza così

int main()
{
    cout << Fattoriale<5>::value;
}

Beh, ora che possiamo avere calcoli così deep a costo zero in esecuzione verrebbe da usarli in questo modo

int main()
{
    unsigned long long n;
    
    cout << "Inserisci il numero di cui calcolare il fattoriale: ";
    cin >> n;
    cout << endl << Fattoriale<n>::value;
}

vero? Beh, no. I Template vengono appunto calcolati in compilazione, e non si conosce il valore di n in compilazione, ma in esecuzione. E no, non basta calcolare già la struttura di un numero molto grande per avere già risultati pronti.

La variante constexpr è invece molto flessibile ed è utilizzabile pure in contesti non costanti.

Problemi con i Template modifica

I Template hanno dei problemi. Il primo è che niente vieta di chiamare una funzione che utilizza certi operatori o certe funzioni membro. Ad esempio chiamate del tipo

int main()
{
	cout << Sum<int*, int*>(0x0, 0x0) << endl; 
}

mettono in crisi il nostro compilatore, non tanto per gli indirizzi nullptr, ma perché tenta di chiamare (int*).operator+(int*), che non esiste. Non abbiamo quindi sicurezza per quanto riguarda i tipi. Il secondo è che il compilatore è costretto a creare una funzione per ogni tipo che chiamiamo, il che produce codice molto ridondante.

Variadic Template modifica

Immaginiamo ora di volere una funzione Sum che sommi un indefinito numero di parametri, ma almeno due. Normalmente è impossibile farlo se non con <cstdarg> I Template semplificano un sacco la cosa:

template <class ...T>
long double Sum(T ...args)
{
	static_assert(1 < sizeof...(args), "Almeno due valori richiesti");
	return (args + ...);
}

args è un pacchetto, come T. Il primo è un pacchetto di argomenti, mentre T di tipi. L’operatore sizeof...() permette di sapere quanti argomenti contenga. La scrittura (args + ...) permette di espandere il pacchetto in (((arg1 + arg2) + arg3) + ...). Anche qui possiamo ricavare errori, prima dall'uso dell'operatore + e poi dal cast a long double. Per quanto riguarda il secondo, ci basta utilizzare il nuovo auto del C++11:

template <class ...T>
auto Sum(T ...args)
{
	static_assert(1 < sizeof...(args), "Almeno due valori richiesti");
	return (args + ...);
}

Qui il tipo di ritorno è dedotto alla fine. Ora è addirittura possibile sommare stringhe!

int main()
{
      // stringhe C++, non stringhe NUL terminated!
	cout << Sum(static_cast<std::string>("Ciao "), "mondo");
}



Va detto che tale metodo rappresentato è solo valido nel C++17. Un buon compilatore vi darà questo errore o simili se state utilizzando C++14 o precedenti: "warning: fold-expressions only available with -std=c++17 or -std=gnu++17". Precedentemente l'espansione era più complessa e per mostrarla si utilizzerà una funzione Print(...) che stamperà a schermo i parametri (una sorta di printf(char*, ...) ma senza la stringa). Questo è il codice che attualmente utilizzeremo:

template <class ...Args>
void Print(Args&& ...args)
{
    (cout << ... << args);
}

Pre C++17 invece si utilizzava questo codice

template <class Arg>
void Print(Arg&& arg)
{
    cout << arg;
}

template <class Arg, class ...Args>
void Print(Arg&& arg, Args&& ...args)
{
    cout << arg;
    Print(args...);
}

O volendo semplificare, post C++11:

template <class Arg, class ...Args>
void Print(Arg&& arg, Args&& ...args)
{
    cout << arg;
    if constexpr(0 != sizeof...(args))
        Print(args...);
}

Comunque più complessa e ricorsiva.