Vai al contenuto

Template di classe e CTAD

Costruisci strutture dati generiche con i template di classe. Comprendi le definizioni dei membri, gli argomenti predefiniti e come il CTAD di C++17 ti permette di omettere gli argomenti template.

Template di classe

Un template di classe definisce una famiglia di classi parametrizzate da uno o più tipi (o valori non di tipo). La libreria standard è costruita sui template di classe: std::vector, std::map, std::optional, std::shared_ptr. Quando scrivi std::vector, il compilatore istanzia l'intera classe con T = int, generando un tipo concreto.

I template di classe seguono le stesse regole dei template di funzione: devono essere definiti negli header e ogni insieme univoco di argomenti template produce un tipo distinto. Stack e Stack sono tipi completamente scorrelati — non condividono alcuna relazione di ereditarietà.

Uno stack generico

Ecco un semplice stack generico. Nota come le funzioni membro definite fuori dal corpo della classe richiedano di ripetere la lista dei parametri template.

stack.hpp
#include <vector>
#include <stdexcept>
#include <iostream>

template <typename T>
class Stack {
private:
    std::vector<T> elements_;

public:
    void push(const T& value);
    void pop();
    const T& top() const;
    bool empty() const { return elements_.empty(); }
    std::size_t size() const { return elements_.size(); }
};

// Member definitions outside the class require the template header
template <typename T>
void Stack<T>::push(const T& value) {
    elements_.push_back(value);
}

template <typename T>
void Stack<T>::pop() {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elements_.pop_back();
}

template <typename T>
const T& Stack<T>::top() const {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elements_.back();
}

int main() {
    Stack<int> intStack;
    intStack.push(42);
    intStack.push(17);
    std::cout << intStack.top() << '\n'; // 17
    intStack.pop();
    std::cout << intStack.top() << '\n'; // 42
}

Parametri multipli e valori predefiniti

I template di classe possono avere più parametri template e argomenti template predefiniti, proprio come i parametri di funzione possono avere valori predefiniti. Gli argomenti predefiniti vengono risolti da sinistra a destra.

stack_defaults.cpp
#include <vector>
#include <deque>
#include <iostream>

// Container defaults to std::deque<T> — matches std::stack's design
template <typename T, typename Container = std::deque<T>>
class Stack {
private:
    Container elements_;

public:
    void push(const T& value) { elements_.push_back(value); }
    void pop()                { elements_.pop_back(); }
    const T& top() const      { return elements_.back(); }
    bool empty() const        { return elements_.empty(); }
};

int main() {
    Stack<int> s1;                         // uses deque<int>
    Stack<int, std::vector<int>> s2;       // uses vector<int>

    s1.push(10);
    s2.push(20);
    std::cout << s1.top() << '\n'; // 10
    std::cout << s2.top() << '\n'; // 20
}

Deduzione degli argomenti dei template di classe (CTAD)

Prima di C++17 dovevi sempre specificare gli argomenti template quando costruivi istanze di un template di classe: std::pair p(1, 3.14);. Il CTAD (Class Template Argument Deduction), introdotto in C++17, permette al compilatore di dedurre gli argomenti template di classe dagli argomenti del costruttore, proprio come avviene per i template di funzione.

Con il CTAD: std::pair p(1, 3.14); — il compilatore deduce std::pair. Funziona con std::vector, std::tuple, std::optional, std::array (C++20) e con i tuoi template di classe.

Il CTAD si basa sulle guide di deduzione — implicite (generate dai costruttori) oppure esplicite (definite dall'utente). Il compilatore prova ogni guida e sceglie quella che corrisponde meglio.

CTAD e guide di deduzione

Le guide di deduzione definite dall'utente indicano al compilatore come mappare gli argomenti del costruttore sugli argomenti template. Sono particolarmente utili quando il tipo dell'argomento del costruttore non corrisponde direttamente al parametro template.

ctad_example.cpp
#include <iostream>
#include <cstddef>

template <typename T, std::size_t N>
struct Array {
    T data[N];
};

// Without a deduction guide, CTAD cannot deduce T and N.
// This explicit deduction guide maps brace-init with N elements
// of the same type T to Array<T, N>.
template <typename First, typename... Rest>
Array(First, Rest...) -> Array<First, 1 + sizeof...(Rest)>;

// Standard library example: std::vector CTAD from iterators
#include <vector>
#include <list>

int main() {
    // CTAD with our custom deduction guide
    Array arr{1, 2, 3, 4, 5};  // deduces Array<int, 5>
    std::cout << arr.data[2] << '\n'; // 3

    // Standard library CTAD examples (C++17)
    std::pair p(42, 3.14);       // pair<int, double>
    std::vector v{1, 2, 3};     // vector<int>
    std::tuple t(1, 2.0, 'a');  // tuple<int, double, char>

    // CTAD from iterator range
    std::list<int> lst{10, 20, 30};
    std::vector v2(lst.begin(), lst.end()); // vector<int>
}

Template membro

Le funzioni membro di un template di classe possono essere a loro volta template, aggiungendo parametri template ulteriori. Questo si chiama template membro. Un caso d'uso comune è abilitare l'assegnamento o la costruzione a partire da un'istanziazione diversa ma imparentata.

Per esempio, potresti voler assegnare uno Stack a uno Stack — ma poiché sono tipi scorrelati, l'assegnamento per copia predefinito non funziona. Un operatore di assegnamento definito come template membro risolve il problema:

template <typename T>
class Stack {
public:
    template <typename U>
    Stack& operator=(const Stack<U>& other);
};

All'interno di questo template membro, U può essere diverso da T. Il corpo può copiare gli elementi da other purché U sia convertibile in T. Questo schema è usato ampiamente nella libreria standard — std::shared_ptr si converte in std::shared_ptr proprio tramite un costruttore definito come template membro.

Insidia

Quando specializzi un template di classe, la specializzazione è una classe completamente nuova — non eredita alcun membro dal template primario. Devi ridefinire ogni funzione membro e ogni dato membro. Dimenticare di ridefinire un membro nella specializzazione provoca un errore di compilazione nel momento in cui quel membro viene usato. Inoltre, la specializzazione parziale è disponibile solo per i template di classe, non per quelli di funzione. Tratteremo la specializzazione in dettaglio nel prossimo capitolo.

Concetti chiave
  • I template di classe definiscono famiglie di classi — ogni insieme univoco di argomenti produce un tipo distinto
  • Le funzioni membro definite fuori dalla classe devono ripetere l'intestazione template e usare ClassName::
  • Gli argomenti template predefiniti si risolvono da sinistra a destra, proprio come gli argomenti di funzione predefiniti
  • Il CTAD (C++17) permette al compilatore di dedurre gli argomenti template di classe dagli argomenti del costruttore
  • Le guide di deduzione esplicite mappano i pattern degli argomenti del costruttore sugli argomenti template
  • I template membro permettono operazioni tra istanziazioni diverse dello stesso template di classe

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Dato `template <typename T, typename Container = std::deque<T>> class Stack`, qual è il tipo di `Stack<int>`?

2. Che cos'è il CTAD (C++17)?

3. Quando specializzi un template di classe, che fine fanno i membri del template primario?