Vai al contenuto

Specializzazione dei template e tag dispatch

Padroneggia la specializzazione totale e parziale, impara quando è meglio ricorrere all'overloading e scopri il tag dispatch e if constexpr come alternative per la ramificazione a tempo di compilazione.

Perché specializzare?

Il template primario fornisce un'implementazione generica che funziona per la maggior parte dei tipi. A volte, però, un tipo specifico richiede un trattamento radicalmente diverso. Per esempio, std::vector è una specializzazione che impacchetta i bool in singoli bit per risparmiare spazio (anche se è considerata da molti un errore della libreria standard).

La specializzazione dei template ti permette di fornire una definizione alternativa per argomenti template specifici. Ne esistono due tipi:

1. Specializzazione totale (esplicita) — fissi tutti i parametri template a tipi specifici.
2. Specializzazione parziale — fissi alcuni parametri o li vincoli (per esempio, specializzi per tutti i tipi puntatore). La specializzazione parziale è disponibile solo per i template di classe, non per quelli di funzione.

Specializzazione totale

Una specializzazione totale fornisce un'implementazione completamente su misura per un insieme specifico di argomenti template. La sintassi usa template <> (una lista di parametri vuota) seguito dalla classe o dalla funzione con argomenti concreti.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Specializzazione parziale

La specializzazione parziale vincola alcuni parametri template lasciandone altri generici. È estremamente potente per intercettare intere famiglie di tipi — tutti i puntatori, tutti gli array, tutte le coppie e così via. Solo i template di classe, struct e variabile supportano la specializzazione parziale.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Insidia

I template di funzione non possono essere specializzati parzialmente, ma solo totalmente. Peggio ancora, le specializzazioni dei template di funzione non partecipano alla risoluzione degli overload come ti aspetteresti. Il compilatore prima seleziona il template base migliore tramite la risoluzione degli overload, e solo dopo verifica se quel template base ha una specializzazione corrispondente.

Questo porta a comportamenti sorprendenti:

template <typename T> void f(T)   { /* #1 - primario */ }
template <typename T> void f(T*)  { /* #2 - overload per i puntatori */ }
template <> void f<int*>(int*)     { /* #3 - specializzazione di #1 */ }

La chiamata f((int*)nullptr) seleziona #2 (l'overload che corrisponde meglio), non #3 (che specializza #1, non #2). La soluzione: usa il normale overloading di funzioni invece della specializzazione dei template di funzione. Se ti serve una ramificazione sui tipi a tempo di compilazione, usa if constexpr o i concept.

Il pattern tag dispatch

Il tag dispatch usa tipi struct vuoti (i "tag") per selezionare gli overload a tempo di compilazione. La libreria standard lo sfrutta ampiamente — std::advance sceglie l'implementazione in base ai tag della categoria di iteratore. Prima di if constexpr era la tecnica dominante per la ramificazione a tempo di compilazione.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: l'alternativa moderna

L'if constexpr di C++17 offre un modo più pulito di ramificare a tempo di compilazione all'interno di una singola funzione. Il ramo scartato non viene istanziato, quindi non deve nemmeno compilare per il tipo dato. Questo sostituisce in larga parte il tag dispatch e SFINAE nei casi semplici.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Buona pratica

Usa questa guida per scegliere come ramificare a tempo di compilazione:

- if constexpr (C++17) — il più semplice, funziona dentro una singola funzione, non richiede pattern particolari. Preferiscilo per la logica diretta basata sui tipi.
- Concept/requires (C++20) — ideali per vincolare gli overload di funzione o interi template. Si autodocumentano e danno messaggi di errore chiari.
- Tag dispatch — resta utile quando hai già una gerarchia di tag (come le categorie di iteratori). I codebase precedenti a C++17 ci si appoggiano moltissimo.
- SFINAE / enable_if — la tecnica più vecchia. Più difficile da leggere, errori criptici. Evitala nel codice nuovo se i concept o if constexpr bastano.
- Specializzazione totale — usala per i template di classe quando ti serve un'implementazione completamente diversa per un tipo specifico.
- Non specializzare mai i template di funzione — usa l'overloading.

Concetti chiave
  • La specializzazione totale (template <>) fornisce un'implementazione su misura per un tipo specifico
  • La specializzazione parziale vincola solo alcuni parametri — è disponibile per i template di classe, non per quelli di funzione
  • Specializzare i template di funzione è fonte di errori — preferisci l'overloading o if constexpr
  • Il tag dispatch usa tipi struct vuoti per selezionare gli overload a tempo di compilazione tramite iterator_traits
  • if constexpr (C++17) è l'alternativa moderna e più pulita: scarta a tempo di compilazione il ramo non preso

Quiz — Metti alla prova le tue conoscenze

(20 XP)

1. Perché è meglio evitare di specializzare i template di funzione?

2. Quali tipi di template supportano la specializzazione parziale?

3. Cosa fa `if constexpr` di diverso rispetto a un `if` normale?