Partager via


Modèles (C++)

Les modèles sont la base de la programmation générique en C++. En tant que langage fortement typé, C++ exige que toutes les variables aient un type spécifique, soit explicitement déclarée par le programmeur, soit déduite par le compilateur. Toutefois, de nombreuses structures de données et algorithmes se présentent de la même façon, quel que soit le type sur lequel ils fonctionnent. Les modèles vous permettent de définir les opérations d’une classe ou d’une fonction et de permettre à l’utilisateur(-trice) de spécifier les types concrets sur utilisant ces opérations.

Définir et utiliser des modèles

Un modèle est une construction qui génère un type ou une fonction ordinaire au moment de la compilation en fonction des arguments que l’utilisateur(-trice) fournit pour les paramètres du modèle. Par exemple, vous pouvez définir un modèle de fonction comme suit :

template <typename T>
T minimum(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

Le code ci-dessus décrit un modèle pour une fonction générique avec un paramètre de type unique T, dont la valeur de retour et les paramètres d’appel (lhs et rhs) sont tous de ce type. Vous pouvez nommer un paramètre de type comme vous le souhaitez, mais par convention, les lettres majuscules uniques sont les plus couramment utilisées. T est un paramètre de modèle ; le mot clé typename indique que ce paramètre est un espace réservé pour un type. Lorsque la fonction est appelée, le compilateur remplace chaque instance de T par l’argument de type concret spécifié par l’utilisateur(-trice) ou déduit par le compilateur. Le processus dans lequel le compilateur génère une classe ou une fonction à partir d’un modèle est appelé instanciation de modèle; minimum<int> est une instanciation du modèle minimum<T>.

Ailleurs, un(e) utilisateur(-trice) peut déclarer une instance du modèle spécialisé pour int. Supposons que get_a() et get_b() sont des fonctions qui retournent une int :

int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);

Toutefois, étant donné qu’il s’agit d’un modèle de fonction et que le compilateur peut déduire le type de T des arguments un et b, vous pouvez l’appeler comme une fonction ordinaire :

int i = minimum(a, b);

Lorsque le compilateur rencontre cette dernière instruction, il génère une nouvelle fonction dans laquelle chaque occurrence de T dans le modèle est remplacée par int :

int minimum(const int& lhs, const int& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

Les règles relatives à la façon dont le compilateur effectue une déduction de type dans les modèles de fonction sont basées sur les règles des fonctions ordinaires. Pour plus d’informations, consultez Résolution de surcharge des appels de modèles de fonctions.

Paramètres de type

Dans le minimummodèle ci-dessus, notez que le paramètre de type T n’est qualifié d’aucune façon tant qu’il n’est pas utilisé dans les paramètres d’appel de fonction, où les qualificateurs const et référence sont ajoutés.

Il n’existe aucune limite pratique au nombre de paramètres de type. Séparez les paramètres multiples par des virgules :

template <typename T, typename U, typename V> class Foo{};

Le mot clé class est équivalent à typename dans ce contexte. Vous pouvez exprimer l’exemple précédent comme suit :

template <class T, class U, class V> class Foo{};

Vous pouvez utiliser l’opérateur de points de suspension (...) pour définir un modèle qui prend un nombre arbitraire de zéro ou plusieurs paramètres de type :

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;

Tout type intégré ou défini par l’utilisateur(-trice) peut être utilisé comme argument de type. Par exemple, vous pouvez utiliser std::vector dans la bibliothèque standard pour stocker des variables de type int, double, std::string, MyClass, const MyClass*, MyClass&, et ainsi de suite. La restriction principale lors de l’utilisation de modèles est qu’un argument de type doit prendre en charge toutes les opérations appliquées aux paramètres de type. Par exemple, si nous appelons minimum à l’aide de MyClass comme dans cet exemple :

class MyClass
{
public:
    int num;
    std::wstring description;
};

int main()
{
    MyClass mc1 {1, L"hello"};
    MyClass mc2 {2, L"goodbye"};
    auto result = minimum(mc1, mc2); // Error! C2678
}

Une erreur du compilateur est générée, car MyClass ne fournit pas de surcharge pour l’opérateur <.

Il n’existe aucune exigence inhérente que les arguments de type d’un modèle particulier appartiennent tous à la même hiérarchie d’objets, bien que vous puissiez définir un modèle qui applique une telle restriction. Vous pouvez combiner des techniques orientées objet avec des modèles. Par exemple, vous pouvez stocker un dérivé* dans un vecteur<Base*>. Notez que les arguments doivent être des pointeurs

vector<MyClass*> vec;
   MyDerived d(3, L"back again", time(0));
   vec.push_back(&d);

   // or more realistically:
   vector<shared_ptr<MyClass>> vec2;
   vec2.push_back(make_shared<MyDerived>());

Les exigences de base qui std::vector et d’autres conteneurs de bibliothèque standard s’imposent sur les éléments de T est que T être assignables de copie et constructibles de copie.

Paramètres de non-type

Contrairement aux types génériques dans d’autres langages tels que C# et Java, les modèles C++ prennent en charge paramètres non de type, également appelés paramètres de valeur. Par exemple, vous pouvez fournir une valeur intégrale constante pour spécifier la longueur d’un tableau, comme avec cet exemple similaire à la classe std::array dans la bibliothèque standard :

template<typename T, size_t L>
class MyArray
{
    T arr[L];
public:
    MyArray() { ... }
};

Notez la syntaxe dans la déclaration de modèle. La valeur size_t est passée en tant qu’argument de modèle au moment de la compilation et doit être const ou une expression constexpr. Vous l’utilisez comme suit :

MyArray<MyClass*, 10> arr;

D’autres types de valeurs, notamment les pointeurs et les références, peuvent être passés en tant que paramètres non de type. Par exemple, vous pouvez passer un pointeur vers une fonction ou un objet de fonction pour personnaliser une opération à l’intérieur du code du modèle.

Déduction de type pour les paramètres de modèle non typés

Dans Visual Studio 2017 et versions ultérieures, et en mode /std:c++17 ou version ultérieure, le compilateur déduit le type d’un argument de modèle non de type déclaré avec auto :

template <auto x> constexpr auto constant = x;

auto v1 = constant<5>;      // v1 == 5, decltype(v1) is int
auto v2 = constant<true>;   // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>;    // v3 == 'a', decltype(v3) is char

Modèles en tant que paramètres de modèles

Un modèle peut être un paramètre de modèle. Dans cet exemple, MyClass2 a deux paramètres de modèle : un paramètre typename T et un paramètre de modèle Arr :

template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
    U u; //Error. U not in scope
};

Étant donné que le paramètre Arr lui-même n’a pas de corps, ses noms de paramètres ne sont pas nécessaires. En fait, il s’agit d’une erreur de référence au nom typique Arr ou les noms de paramètres de classe à partir du corps de MyClass2. Pour cette raison, les noms de paramètres de typeArr peuvent être omis, comme illustré dans cet exemple :

template<typename T, template<typename, int> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
};

Arguments du modèle par défaut

Les modèles de classe et de fonction peuvent avoir des arguments par défaut. Lorsqu’un modèle a un argument par défaut, vous pouvez le laisser non spécifié lorsque vous l’utilisez. Par exemple, le modèle std::vector a un argument par défaut pour l’allocateur(-trice) :

template <class T, class Allocator = allocator<T>> class vector;

Dans la plupart des cas, la classe std::allocator par défaut est acceptable. Vous utilisez donc un vecteur comme suit :

vector<int> myInts;

Toutefois, si nécessaire, vous pouvez spécifier un(e) allocateur(-trice) personnalisé(e) comme suit :

vector<int, MyAllocator> ints;

Pour plusieurs arguments template, tous les arguments après le premier argument par défaut doivent avoir des arguments par défaut.

Lorsque vous utilisez un modèle dont les paramètres sont tous par défaut, utilisez des crochets d’angle vides :

template<typename A = int, typename B = double>
class Bar
{
    //...
};
...
int main()
{
    Bar<> bar; // use all default type arguments
}

Spécialisation de modèle

Dans certains cas, il n’est pas possible ou souhaitable qu’un modèle définisse exactement le même code pour n’importe quel type. Par exemple, vous pouvez définir un chemin de code à exécuter uniquement si l’argument de type est un pointeur, un std::wstring ou un type dérivé d’une classe de base particulière. Dans ce cas, vous pouvez définir une spécialisation du modèle pour ce type particulier. Lorsqu’un(e) utilisateur(-trice) instancie le modèle avec ce type, le compilateur utilise la spécialisation pour générer la classe et pour tous les autres types, le compilateur choisit le modèle plus général. Les spécialisations dans lesquelles tous les paramètres sont spécialisés sont spécialisations complètes. Si seuls certains paramètres sont spécialisés, il est appelé spécialisation partielle.

template <typename K, typename V>
class MyMap{/*...*/};

// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization

Un modèle peut avoir n’importe quel nombre de spécialisations tant que chaque paramètre de type spécialisé est unique. Seuls les modèles de classe peuvent être partiellement spécialisés. Toutes les spécialisations complètes et partielles d’un modèle doivent être déclarées dans le même espace de noms que le modèle d’origine.

Pour plus d’informations, consultez Spécialisation de modèle.