Aller au contenu

Spécialisation de templates et tag dispatch

Maîtrisez la spécialisation complète et partielle, apprenez quand privilégier la surcharge, et découvrez le tag dispatch et if constexpr comme alternatives pour le branchement à la compilation.

Pourquoi spécialiser ?

Le template principal fournit une implémentation générique qui convient à la plupart des types. Mais parfois, un type particulier nécessite un traitement fondamentalement différent. Par exemple, std::vector est une spécialisation qui compacte les booléens en bits pour économiser de l'espace (même si elle est largement considérée comme une erreur de la bibliothèque standard).

La spécialisation de template vous permet de fournir une définition alternative pour des arguments de template précis. Il en existe deux sortes :

1. Spécialisation complète (explicite) — vous fixez tous les paramètres de template à des types précis.
2. Spécialisation partielle — vous fixez certains paramètres ou vous les contraignez (par exemple, une spécialisation pour tous les types pointeurs). La spécialisation partielle n'est disponible que pour les templates de classes, pas pour les templates de fonctions.

Spécialisation complète

Une spécialisation complète fournit une implémentation entièrement personnalisée pour un ensemble précis d'arguments de template. La syntaxe utilise template <> (une liste de paramètres vide) suivi de la classe ou de la fonction avec des arguments concrets.

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
}

Spécialisation partielle

La spécialisation partielle contraint certains paramètres de template tout en laissant les autres génériques. C'est extrêmement puissant pour cibler des familles de types — tous les pointeurs, tous les tableaux, toutes les paires, etc. Seuls les templates de classes/structs et de variables prennent en charge la spécialisation partielle.

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
}
Piège

Les templates de fonctions ne peuvent pas être partiellement spécialisés — seulement complètement. Pire encore, les spécialisations de templates de fonctions ne participent pas à la résolution de surcharge comme vous pourriez vous y attendre. Le compilateur sélectionne d'abord le meilleur template de base par résolution de surcharge, et vérifie seulement ensuite si ce template de base possède une spécialisation correspondante.

Cela conduit à un comportement surprenant :

template <typename T> void f(T)   { /* #1 - template principal */ }
template <typename T> void f(T*)  { /* #2 - surcharge pour les pointeurs */ }
template <> void f<int*>(int*)     { /* #3 - spécialisation de #1 */ }

Appeler f((int*)nullptr) sélectionne #2 (la meilleure surcharge), et non #3 (qui spécialise #1, pas #2). La solution : utilisez la surcharge de fonctions classique plutôt que la spécialisation de templates de fonctions. Si vous avez besoin d'un branchement sur les types à la compilation, utilisez if constexpr ou les concepts.

Le motif tag dispatch

Le tag dispatch (aiguillage par étiquette) utilise des types struct vides (des « tags ») pour sélectionner des surcharges à la compilation. La bibliothèque standard s'en sert abondamment — std::advance aiguille selon les tags de catégorie d'itérateur. C'était la technique dominante de branchement à la compilation avant if constexpr.

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'alternative moderne

Le if constexpr de C++17 offre une manière plus propre de brancher à la compilation au sein d'une seule fonction. La branche écartée n'est pas instanciée : elle n'a donc pas besoin de compiler pour le type donné. Dans les cas simples, cela remplace largement le tag dispatch et SFINAE.

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"
}
Bonne pratique

Utilisez ce guide de décision pour le branchement à la compilation :

- if constexpr (C++17) — le plus simple, fonctionne au sein d'une seule fonction, sans motif particulier. Privilégiez-le pour une logique simple basée sur les types.
- Concepts/requires (C++20) — idéal pour contraindre des surcharges de fonctions ou des templates entiers. Auto-documenté et donne des messages d'erreur clairs.
- Tag dispatch — toujours utile quand vous disposez d'une hiérarchie de tags existante (comme les catégories d'itérateurs). Les bases de code antérieures à C++17 s'appuient beaucoup dessus.
- SFINAE / enable_if — la technique la plus ancienne. Plus difficile à lire, erreurs cryptiques. À éviter dans le nouveau code si les concepts ou if constexpr conviennent.
- Spécialisation complète — à utiliser pour les templates de classes quand vous avez besoin d'une implémentation complètement différente pour un type précis.
- N'utilisez jamais la spécialisation de templates de fonctions — utilisez plutôt la surcharge.

Points clés
  • La spécialisation complète (template <>) fournit une implémentation personnalisée pour un type précis
  • La spécialisation partielle contraint certains paramètres — disponible uniquement pour les templates de classes, pas pour les templates de fonctions
  • La spécialisation de templates de fonctions est source d'erreurs — préférez la surcharge ou if constexpr
  • Le tag dispatch utilise des types struct vides pour sélectionner des surcharges à la compilation via iterator_traits
  • if constexpr (C++17) est l'alternative moderne et plus propre, qui écarte à la compilation les branches non prises

Quiz — Testez vos connaissances

(20 XP)

1. Pourquoi devriez-vous éviter de spécialiser les templates de fonctions ?

2. Quels types de templates prennent en charge la spécialisation partielle ?

3. Qu'est-ce que `if constexpr` fait différemment d'un `if` classique ?