Template e programmazione generica in C++

Template e programmazione generica in C++

La programmazione generica permette di scrivere codice che funziona con tipi diversi senza doverlo riscrivere per ciascuno di essi. Lo strumento che il C++ offre a questo scopo è il template, ovvero un modello a partire dal quale il compilatore genera automaticamente versioni specializzate per i tipi effettivamente utilizzati. I template stanno alla base della libreria standard e rappresentano uno degli aspetti più caratteristici e potenti del linguaggio.

Il problema della duplicazione

Senza i template, una funzione che opera su un tipo specifico andrebbe duplicata per ogni tipo da supportare. Una funzione che restituisce il massimo tra due interi e una che fa lo stesso con due numeri in virgola mobile avrebbero corpo identico, differendo solo per il tipo. Questa duplicazione è tediosa e soggetta a errori, e i template la eliminano alla radice.

Template di funzione

Un template di funzione si dichiara premettendo la parola chiave template seguita dai parametri di tipo racchiusi tra parentesi angolari. Il parametro di tipo, introdotto da typename, funge da segnaposto per un tipo qualsiasi che verrà determinato al momento della chiamata. Il compilatore genera la versione concreta della funzione in base ai tipi degli argomenti.

#include <iostream>

// Un solo modello per tutti i tipi confrontabili
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7) << std::endl;        // Versione per int
    std::cout << maximum(2.5, 1.8) << std::endl;    // Versione per double
    return 0;
}

Il compilatore deduce automaticamente il tipo dai parametri passati, in un processo detto deduzione degli argomenti. È anche possibile specificare esplicitamente il tipo tra parentesi angolari, ad esempio scrivendo la chiamata nella forma con il tipo indicato, quando la deduzione non è possibile o si desidera forzarne uno specifico.

Template di classe

Non solo le funzioni, ma anche le classi possono essere parametrizzate rispetto a uno o più tipi. Un template di classe permette di definire strutture dati generiche, come contenitori che funzionano con qualsiasi tipo di elemento. La maggior parte dei contenitori della libreria standard, tra cui std::vector, è proprio un template di classe.

// Un contenitore generico che conserva un singolo valore
template <typename T>
class Box {
private:
    T content;

public:
    Box(const T& value) : content(value) {}

    T get() const {
        return content;
    }

    void set(const T& value) {
        content = value;
    }
};

int main() {
    Box<int> intBox(5);          // Box che contiene un intero
    Box<std::string> textBox("ciao");   // Box che contiene una stringa

    std::cout << intBox.get() << std::endl;
    return 0;
}

Per i template di classe, a differenza di quelli di funzione, di norma il tipo va specificato esplicitamente tra parentesi angolari al momento dell'istanziazione, anche se le versioni più recenti dello standard hanno introdotto forme di deduzione automatica anche in questo contesto.

Parametri multipli e parametri non di tipo

Un template può avere più parametri di tipo, separati da virgole, per modellare strutture che combinano tipi diversi, come una coppia chiave-valore. Inoltre, i template possono accettare parametri che non sono tipi ma valori costanti noti in fase di compilazione, ad esempio una dimensione: è proprio così che funziona std::array, parametrizzato sia sul tipo degli elementi sia sul loro numero.

// Due parametri di tipo per una coppia generica
template <typename K, typename V>
class Pair {
public:
    K key;
    V value;

    Pair(const K& k, const V& v) : key(k), value(v) {}
};

// Parametro non di tipo: una dimensione costante
template <typename T, int Size>
class FixedBuffer {
private:
    T data[Size];   // La dimensione e nota a compile time
};

Vantaggi e considerazioni

I template generano codice efficiente, perché ogni versione viene specializzata per il tipo concreto senza alcun costo aggiuntivo a runtime. Permettono di scrivere algoritmi e strutture dati riutilizzabili una sola volta e di applicarli a innumerevoli tipi. Lo svantaggio principale è che il codice dei template deve essere generalmente disponibile nei file di intestazione, e i messaggi di errore possono risultare verbosi quando un tipo non soddisfa le operazioni richieste dal template.

La programmazione generica con i template è ciò che rende la libreria standard tanto potente e versatile. Comprenderne i fondamenti apre la strada all'uso consapevole dei contenitori, degli iteratori e degli algoritmi, che affronteremo negli articoli successivi.