Aller au contenu

Templates de classes et CTAD

Construisez des structures de données génériques avec les templates de classes. Comprenez les définitions de membres, les arguments par défaut et comment la CTAD de C++17 vous permet d'omettre les arguments de template.

Templates de classes

Un template de classe définit une famille de classes paramétrées par un ou plusieurs types (ou valeurs non-type). La bibliothèque standard repose sur les templates de classes : std::vector, std::map, std::optional, std::shared_ptr. Lorsque vous écrivez std::vector, le compilateur instancie la classe entière avec T = int et génère un type concret.

Les templates de classes obéissent aux mêmes règles que les templates de fonctions : ils doivent être définis dans les en-têtes, et chaque ensemble unique d'arguments de template produit un type distinct. Stack et Stack sont des types totalement indépendants — ils n'ont aucune relation d'héritage.

Une pile générique

Voici une pile générique simple. Remarquez que les fonctions membres définies en dehors du corps de la classe doivent répéter la liste des paramètres de 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
}

Paramètres multiples et valeurs par défaut

Les templates de classes peuvent avoir plusieurs paramètres de template et des arguments de template par défaut, tout comme les paramètres de fonction peuvent avoir des valeurs par défaut. Les arguments par défaut sont résolus de gauche à droite.

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
}

Déduction des arguments de template de classe (CTAD)

Avant C++17, il fallait toujours préciser les arguments de template lors de la construction d'instances de templates de classes : std::pair p(1, 3.14);. La CTAD (Class Template Argument Deduction, déduction des arguments de template de classe), introduite en C++17, permet au compilateur de déduire les arguments d'un template de classe à partir des arguments du constructeur, exactement comme pour la déduction des arguments d'un template de fonction.

Avec la CTAD : std::pair p(1, 3.14); — le compilateur déduit std::pair. Cela fonctionne pour std::vector, std::tuple, std::optional, std::array (C++20) et vos propres templates de classes.

La CTAD s'appuie sur des guides de déduction — soit implicites (générés à partir des constructeurs), soit explicites (définis par l'utilisateur). Le compilateur essaie chaque guide et choisit la meilleure correspondance.

CTAD et guides de déduction

Les guides de déduction définis par l'utilisateur indiquent au compilateur comment faire correspondre les arguments du constructeur aux arguments de template. Ils sont particulièrement utiles lorsque le type de l'argument du constructeur ne correspond pas directement au paramètre de 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>
}

Templates membres

Les fonctions membres d'un template de classe peuvent elles-mêmes être des templates et ajouter des paramètres de template supplémentaires. On parle alors de template membre. Un cas d'usage courant consiste à permettre l'affectation ou la construction à partir d'une instanciation apparentée mais différente.

Par exemple, vous pourriez vouloir affecter un Stack à un Stack — mais comme ce sont des types sans rapport, l'affectation par copie par défaut ne fonctionnera pas. Un opérateur d'affectation sous forme de template membre résout ce problème :

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

À l'intérieur de ce template membre, U peut différer de T. Le corps peut copier les éléments de other tant que U est convertible en T. Ce motif est largement utilisé dans la bibliothèque standard — std::shared_ptr se convertit en std::shared_ptr grâce à un constructeur template membre.

Piège

Lorsque vous spécialisez un template de classe, la spécialisation est une classe entièrement nouvelle — elle n'hérite d'aucun membre du template principal. Vous devez redéfinir chaque fonction membre et chaque donnée membre. Oublier de redéfinir un membre dans la spécialisation provoque une erreur de compilation dès que ce membre est utilisé. Par ailleurs, la spécialisation partielle n'est disponible que pour les templates de classes, pas pour les templates de fonctions. Nous verrons la spécialisation en détail dans le prochain chapitre.

Points clés
  • Les templates de classes définissent des familles de classes — chaque ensemble unique d'arguments produit un type distinct
  • Les fonctions membres définies en dehors de la classe doivent répéter l'en-tête template et utiliser ClassName::
  • Les arguments de template par défaut fonctionnent de gauche à droite, comme les arguments par défaut des fonctions
  • La CTAD (C++17) permet au compilateur de déduire les arguments d'un template de classe à partir des arguments du constructeur
  • Les guides de déduction explicites associent des motifs d'arguments de constructeur à des arguments de template
  • Les templates membres permettent des opérations entre différentes instanciations d'un même template de classe

Quiz — Testez vos connaissances

(15 XP)

1. Étant donné `template <typename T, typename Container = std::deque<T>> class Stack`, quel est le type de `Stack<int>` ?

2. Qu'est-ce que la CTAD (C++17) ?

3. Lorsque vous spécialisez un template de classe, qu'advient-il des membres du template principal ?