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 de spécifier les types concrets sur utilisant ces opérations.

Définition et utilisation de 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 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 ; l’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 l’argument de T type concret spécifié par l’utilisateur 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 utilisateur 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 des T arguments a 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 La résolution de surcharge des appels de modèle de fonction.

Paramètres de type

Dans le minimum modè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 plusieurs paramètres par virgules :

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

La mot clé class équivaut à typename 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 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, stddouble ::string, MyClassconstMyClass*, 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 l’utilisation minimumMyClass 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 elle 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 dont T il s’agit d’être T assignables à la copie et pouvant être construits par copie.

Paramètres non de type

Contrairement aux types génériques dans d’autres langages tels que C# et Java, les modèles C++ prennent en charge les 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 size_t valeur est passée en tant qu’argument de modèle au moment de la compilation et doit être const ou une constexpr expression. 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 de type

Dans Visual Studio 2017 et versions ultérieures, et en /std:c++17 mode 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èle

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 pour faire référence aux noms de paramètres de typename ou de classe d’Arr à partir du corps de MyClass2. Pour cette raison, les noms de paramètres de type d’Arr 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 de 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 :

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 allocateur personnalisé 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 utilisateur 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 des spécialisations complètes. Si seuls certains des paramètres sont spécialisés, il s’agit d’une 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 du modèle.