Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Langage C++
  3. Généricité, template, auto
Extrait - Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition)
Extraits du livre
Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition) Revenir à la page d'achat du livre

Généricité, template, auto

Principe du template

Le template, nommé parfois patron ou modèle en français, associé à la fonction et à la classe dote le C++ d’un outil très puissant au fondement d’une programmation générique. Depuis C++11, notamment avec C++20, son utilisation s’affine et s’approfondit avec l’introduction de nouveaux mots-clés et les développements de la bibliothèque standard <concepts> (pour accéder aux fonctionnalités de C++20 sous Visual Studio se reporter au chapitre Premiers programmes, section Activer C++20).

Le template permet d’écrire des fonctions qui utilisent des paramètres ou une valeur de retour précisés au moment de l’appel de la fonction. Ce principe s’utilise aussi pour des classes qui peuvent recourir à des types d’éléments définis à la déclaration d’un objet. Typiquement, une classe pile par exemple, capable de produire des piles de n’importe quel type d’éléments, le type de l’élément est spécifié avec la déclaration d’un objet pile (pile de int, float, objets, etc.).

Les expressions template sont toujours définies en global hors bloc et évaluées à la compilation, de sorte qu’un template ne peut pas utiliser d’éléments dépendants de l’exécution du programme. Dans certaines expressions, la syntaxe du template apparaît un peu lourde et demande que l’on s’y habitue, presque comme s’il s’agissait d’un langage à part. La section Concepts (C++20) complète cet exposé avec des notions sur le renforcement de l’écriture des templates par l’introduction de "concept", nouveau mot-clé...

Template de fonction

En C++, les fonctions peuvent disposer d’un type générique de paramètre, c’est-à-dire des paramètres dont le type reste indifférent lors de la définition de la fonction mais qui sont déterminés plus tard avec l’appel de la fonction.

1. Définir une fonction générique

La définition d’une fonction générique s’effectue en faisant précéder la fonction de l’expression :

template <class nomType> 

nomType est le nom donné au type générique. Par exemple :

template <class T> T fonct(T a, T b) 
{ 
    return (a<b) ? a : b; 
} 

Le mot-clé class peut être remplacé par typename et c’est, semble-t-il, plus clair dans ce contexte. Ce qui donne :

#include <iostream> 
 
template <typename T> T fonct(T a, T b) 
{ 
    return (a<b) ? a : b; 
} 

Cette fonction retourne la valeur minimum sur les deux passées en paramètres. T correspond au type générique. Le type effectif est celui spécifié au moment de l’appel avec les valeurs données aux paramètres :

int main() 
{ 
    // avec des float 
    std::cout << fonct(1.5f, 6.2f) << std::endl; 
 
    // avec des int 
    std::cout << fonct(200, 100) << std::endl; 
 
    // avec des char 
    std::cout << fonct('z', 'a') << std::endl; 
 
    std::cin.get(); 
    return 0; 
} 

Éventuellement, il est possible aussi...

Template de classe

1. Syntaxe

a. Syntaxe générale

La syntaxe pour définir une classe générique est la suivante. Soit T le nom du type générique et maClasse le nom de la classe :

template <class T> class maClasse  { ... }; 

Le mot-clé class peut avantageusement se remplacer par typename. Dans cette situation, les deux fonctionnent à l’identique et nous écrirons plutôt :

template <typenanme T> class maClasse  { ... }; 

À la déclaration d’un objet, il faut ensuite spécifier le ou les types souhaités :

maClasse<int> e1; 

Le type choisi peut être un type primitif (char, short, int, etc.) ou une classe. Pointeurs et références sont admis (pour les pointeurs et références, se reporter au chapitre Pointeurs et pour l’utilisation en template un exemple est donné au chapitre Pointeurs et références dans la classe dans la section Questions diverses - Spécialiser une classe générique en pointeur). 

Dans la classe, les données de type générique se déclarent avec le nom donné au type générique. Il en va de même pour les paramètres de fonction. Les déclarations de fonction sont ordinaires :

template <typename T> class maClasse 
{ 
    int val; 
 
    // déclaration propriété de type T 
    T valGenerique; 
 
    // déclaration ordinaire de fonction 
    void affiche(); 
 
    // fonction avec retour et un paramètre de type T 
    T fonct(int a, T b); 
}; 

En revanche, pour la définition...

Template variadique et paquet de paramètres

1. Principe : l’opérateur variadique (…)

L’opérateur variadique ..., utilisé en C pour écrire des fonctions à nombre variable de paramètres comme printf, permet de définir dans un template un type comprenant un nombre variable d’éléments éventuellement, mais pas nécessairement différents. La syntaxe est la suivante :

template<class...Arguments> 
void f(Arguments...arg) {/* contenu fonction */} 

ou, synonyme dans ce contexte :

template<typename...Arguments> 
void f(Arguments...arg) {/* contenu fonction */} 

L’opérateur variadique ... placé dans le template après les instructions class ou typename signifie que le paramètre qui suit est un type "paquet de paramètres" (parameter pack dans les documentations en anglais). Les espaces qui séparent nom et opérateur ne comptent pas, les déclarations suivantes sont équivalentes :

template<typename ...Arguments>, 
template<typename... Arguments> 
template<typename ... Arguments> 

L’opérateur variadique ... placé cette fois après le nom du paramètre template :

Argument... 

va permettre de définir un élément Argument désigné dans les documentations comme "expansion de paquet de paramètres". L’expression :

Arguments...args 

utilisée par la fonction f définit une expansion args de type Arguments.... Ainsi, la fonction générique f possède en paramètre une liste variable de paramètres qui est de type Arguments....

Cette fonction permet des appels comme :

f (); 
f(10); 
f(1, 'A', 2.2, "Bonjour"); 
f("Resultat : "...

Utilités standards à base de templates

Voici sélectionnés quelques éléments génériques standards pratiques et instructifs élaborés en C++ avec des templates.

La classe générique bitset permet d’obtenir facilement un ensemble de bits utilisables comme un ensemble de booléens. Un objet bitset est plus facile à utiliser qu’une variable entière non signée.

Les classes génériques tuple et pair fournissent des ensembles de valeurs sans pour autant obliger à entrer dans les détails de l’implémentation d’une classe. Certes, une simple structure avec le nombre approprié d’éléments fonctionne parfaitement, mais cette alternative fournie en standard s’avère judicieuse dans de nombreuses situations.

Pour finir, nous proposons de visiter des classes inspirées de l’union, les classes variant, optional et any, qui interviennent dans certaines situations.

Pour ce qui concerne bitset, tuple et pair, des informations très complètes et très claires sont fournies sur https://cplusplus.com/reference/. Pour les variant, optional et any, il faut se reporter sur https://en.cppreference.com. Ces informations sont de qualité, mais pas toujours faciles d’accès à ceux qui ne connaissent pas déjà un peu les sujets.

1. La classe bitset

Traditionnellement, en C une séquence de bits s’obtient avec une variable entière non signée, un unsigned int par exemple. Chaque bit de la variable fournit alors l’équivalent d’un booléen accessible uniquement avec les opérateurs bit à bit (se reporter au chapitre Opérations, section Opérations bit à bit). Un objet bitset reprend ce principe avec l’utilisation des opérateurs...

Concepts (C++20)

Les techniques couvertes par la notion C++ de concept viennent renforcer et compléter l’écriture des templates afin de les rendre d’une part plus clairs et lisibles du point de vue du sens et d’autre part plus sûrs, plus sécurisés, dans leurs différents emplois possibles. Cet encadrement de l’écriture des templates s’exerce uniquement à la compilation des programmes. Il permet d’assurer une meilleure visibilité de problèmes potentiels pendant l’écriture de code générique et surtout d’éviter que ces problèmes ne se posent lors de l’exécution. Cette technique s’appuie sur l’utilisation de deux mots-clés, concept et requires, les opérateurs de conjonction ET (&&) et de disjonction OU (||) ainsi que sur l’exploitation de deux bibliothèques :

  • La bibliothèque <type_traits> qui contient un ensemble de classes pour fournir des informations sur les types pendant la compilation.

  • La bibliothèque <concepts> qui offre un ensemble de concepts prédéfinis et applicables à des arguments de template pour la compilation.

Les concepts, objet d’importants développements, deviennent avec la programmation générique un sujet très spécialisé qui s’adosse au langage C++ et mérite une étude complémentaire. D’autres bibliothèques contiennent maintenant des outils les concernant. Par exemple, à propos des traitements de séquences d’éléments, de tableaux ou de conteneurs, il y a les bibliothèques <iterator> et <range>.

Dans le cadre de cet ouvrage, nous nous en tenons à présenter les premières notions d’écriture de concepts, mais l’utilisation...

Spécification auto

1. Principes généraux

a. Variables simples auto

Une variable auto prend le type de la valeur qui lui est passée à sa déclaration. Avec :

auto d = 15 + 1.5; 

le type de d est decltype(15 + 1.5), c’est-à-dire double.

Avec :

auto L = [=](int val) {return d + val; }; 

le type est lambda de la forme function<double(int)>, l’expression auto simplifie l’écriture de :

include<functional> 
std::function<double(int)> L = [=](int val) {return d + val; }; 

Si une variable auto se trouve en paramètre de fonction, son type est déterminé par la valeur passée lors de l’appel de la fonction :

#include<iostream> 
#include<typeinfo> 
 
void fonct(auto a) { std::cout << typeid(a).name() << '\n'; } 
int main() 
{ 
    fonct('A');        // char 
    fonct(100);        // int 
    fonct(12.34);      // double 
 
    std::cin.get(); 
    return 0; 
} 

Si la valeur de retour de fonction est déclarée auto, elle prend le type de la valeur retournée par la première instruction return trouvée, sachant qu’un seul type de retour peut être déduit :

#include<iostream> 
#include<typeinfo> 
auto fonct()  
{  
    switch (rand() % 3) { 
    case 0: return 'A' + rand() % 26; break;        // oui int 
    case 1: return rand() % 100; break;             //...