Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
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. C++
  3. La bibliothèque Standard Template Library
Extrait - C++ Des fondamentaux du langage aux applications (3e édition)
Extraits du livre
C++ Des fondamentaux du langage aux applications (3e édition) Revenir à la page d'achat du livre

La bibliothèque Standard Template Library

Introduction

L’inventeur de C++, Bjarne Stroustrup, a travaillé sur des projets de développement très importants. Les premières applications de C++ ont été réalisées dans le domaine des télécommunications, domaine qui réclame des outils de haut niveau. Certes les classes favorisent l’abstraction, mais un programme n’est pas composé uniquement d’interfaces et d’implémentations.

À la conception du logiciel, le développeur doit déterminer la limite de réutilisation des réalisations précédentes. Derrière le terme réutilisation, on entend souvent le fait de copier/coller certaines parties de code source. Quels sont les éléments qui se prêtent le mieux à cette opération ? Les classes, naturellement, le modèle orienté objet étant construit autour du concept de réutilisation. Mais on trouve également des structures de données, des fonctions spécialisées, des pièces algorithmiques de natures diverses, n’étant pas encore parvenues à la maturité nécessaire à la formation de classes.

Les modules décrivent un découpage assez physique des programmes. Ainsi, un fichier de code source .h ou .cpp peut être considéré comme module....

Organisation des programmes

1. Espaces de noms

Le langage C ne connaît que deux niveaux de portée : le niveau global, auquel la fonction main() appartient, et le niveau local, destiné aux instructions et aux variables locales. Avec l’apparition de classes, un niveau supplémentaire s’est installé, celui destiné à l’enregistrement des champs et des méthodes. Puis l’introduction de la dérivation (héritage) et des membres statiques a encore nuancé la palette des niveaux de portée.

Pour les raisons évoquées en introduction, il devenait nécessaire de structurer l’espace global. Pour n’en retenir qu’une, l’espace global est trop risqué pour le rangement de variables et de fonctions provenant de programmes anciens. Les conflits sont inévitables.

On peut alors partitionner cet espace global à l’aide d’espaces de noms :

namespace Batiment 
{ 
  double longueur; 
 
  void mesurer() 
  { 
    longueur=50.3; 
  } 
} ; 
 
namespace Chaines 
{ 
 int longueur; 
 
  void calcule_longueur(char*s) 
  { 
    longueur=strlen(s); 
  } 
} ; 

Deux espaces de noms, Batiment et Chaines, contiennent tous les deux une variable nommée longueur, d’ailleurs de type différent. Les fonctions mesurer() et calcule_longueur() utilisent toujours la bonne version, car la règle d’accessibilité est également vérifiée dans les espaces de noms : le compilateur cherche toujours la version la plus proche.

Pour utiliser l’une ou l’autre de ces fonctions, la fonction main() doit recourir à l’opération de résolution de portée :: ou bien à une instruction using :

int main(int argc, char* argv[]) 
{ 
  Batiment::mesurer(); 
  printf("La longueur du bâtiment est %g\n",Batiment::longueur); 
 
  using Chaines::longueur; 
  Chaines::calcule_longueur("bonjour"); 
  printf("La longueur de la chaîne est %d\n",longueur); 
  return 0; ...

Flux C++ (entrées-sorties)

La STL gère de nombreux aspects des entrées-sorties. Elle inaugure une façon de programmer pour rendre persistants les nouveaux types définis à l’aide du langage C++.

L’équipe qui l’a conçue à la fin des années 80 a eu le souci d’être conforme aux techniques en vigueur et de produire un travail qui résisterait au fil des ans.

Il faut reconnaître que la gestion des fichiers a énormément évolué depuis l’introduction de la bibliothèque standard : les bases de données relationnelles ont remplacé les fichiers structurés, et les interfaces graphiques se sont imposées face aux consoles orientées caractères.

Toutefois, les axes pris pour le développement de la STL étaient les bons. Si l’utilisation des flux est un peu tombée en désuétude, leur étude permet d’y voir plus clair pour produire une nouvelle génération d’entrées-sorties. Aussi, le terminal en mode caractères continue son existence, la vitalité des systèmes Linux en est la preuve.

1. Généralités

Pour bien commencer l’apprentissage des entrées-sorties, il faut faire la différence entre fichier et flux. Un fichier est caractérisé par un nom, un emplacement, des droits d’accès et parfois aussi un périphérique. Un flux (stream en anglais) est un contenu, une information qui est lue ou écrite par le programme. Cette information peut être de plus ou moins haut niveau. À la base, on trouve naturellement l’octet, puis celui-ci se spécialise en donnée de type entier, décimal, booléen, chaîne... Enfin, on peut créer des enregistrements composés d’informations très diverses. Il est tout à fait logique de considérer que la forme de ces enregistrements correspond à la formation d’une classe, c’est-à-dire d’un type au sens C++.

Les flux C++ (que l’on appelle parfois flots en langue française) sont organisés en trois niveaux ; le premier, le plus abstrait, regroupe les ios_base, format d’entrée-sortie indépendant de l’état...

Classe string pour la représentation des chaînes de caractères

C’est un fait étonnant, la majorité des traités d’algorithmie n’étudient pas les chaînes en tant que telles. La structure de données s’en rapprochant le plus reste le tableau pour lequel on a imaginé une grande quantité de problèmes et de solutions.

Le langage C est resté fidèle à cette approche et considère les chaînes comme des tableaux de caractères. Ses concepteurs ont fait deux choix importants : la longueur d’une chaîne est limitée à celle allouée pour le tableau, et le codage est celui des caractères du C, utilisant la table ASCII. Dans la mesure où il n’existe pas de moyen de déterminer la taille d’un tableau autrement qu’en utilisant une variable supplémentaire, les concepteurs du langage C ont imaginé de terminer leurs chaînes par un caractère spécial, de valeur nulle. Il est vrai que ce caractère n’a pas de fonction dans la table ASCII, mais les chaînes du C sont devenues très spécialisées, donc très loin de l’algorithmie générale.

L’auteur de C++, Bjarne Stroustrup, a souhaité pour son langage une compatibilité avec le langage C mais aussi une amélioration du codage prenant en compte différents formats de codage, ASCII ou non.

1. Représentation des chaînes dans la STL

Pour la STL, une chaîne est un ensemble ordonné de caractères. Une chaîne s’apparente donc fortement au vector, classe également présente dans la bibliothèque. Toutefois, la chaîne développe des accès et des traitements qui lui sont propres, soutenant ainsi mieux les algorithmes traduits en C++.

Les chaînes de la bibliothèque standard utilisent une classe de caractères pour s’affranchir du codage. La STL fournit le support pour les caractères ASCII (char) et pour les caractères étendus (wchar_t), mais on pourrait très bien envisager de développer d’autres formats destinés à des algorithmes à base de chaînes. Le génie génétique emploie des chaînes composées...

Conteneurs dynamiques

Une fonction essentielle de la bibliothèque standard est de fournir des mécanismes pour supporter les algorithmes avec la meilleure efficacité possible. Cet énoncé comporte plusieurs objectifs contradictoires. Les algorithmes réclament de la généricité, autrement dit des méthodes de travail indépendantes du type de données à manipuler. Le langage C utilisait volontiers les pointeurs void* pour garantir la généricité mais cette approche entraîne une perte d’efficacité importante dans le contrôle des types, une complication du code et finalement de piètres performances. L’efficacité réclamée pour STL. s’obtient au prix d’une conception rigoureuse et de contrôles subtils des types. Certes, le résultat est un compromis entre des attentes parfois opposées mais il est assez probant pour être utilisé à l’élaboration d’applications dont la sûreté de fonctionnement est impérative.

Les concepteurs de la STL ont utilisé les modèles de classes pour développer la généricité. Les modèles de classes et de fonctions sont étudiés en détail au chapitre Programmation orientée objet, et leur emploi est assez simple. Une classe est instanciée à partir de son modèle en fournissant les paramètres attendus, généralement le type de données effectivement pris en compte pour l’implémentation de la classe. Il faut absolument différencier cette approche de l’utilisation de macros (#define) qui provoque des résultats inattendus. Ces macros se révèlent très peu sûres d’emploi.

Une idée originale dans la construction de la bibliothèque standard est la corrélation entre les conteneurs de données et les algorithmes s’appliquant à ces conteneurs. Une lecture comparée de différents manuels d’algorithmie débouche sur la conclusion que les structures de données sont toujours un peu les mêmes, ainsi que les algorithmes s’appliquant à ces structures. Il n’était donc pas opportun de concevoir...

Travaux pratiques

L’interprète tiny-lisp s’appuie largement sur la STL. Voici des précisions sur la classe Variant implémentée à grand renfort d’objets issus de la bibliothèque standard.

1. La classe Variant

Variant est le type de données universel de tiny-lisp. Cela peut être un symbole, un nombre, une liste (de Variant), une procédure.

images/04NEWRI09.png

Dans tiny-lisp, l’objet Variant fait partie d’un environnement, un conteneur doté d’une table des symboles. Cette structure est nécessaire à l’exécution des fonctions et des lambda-expressions LISP pour passer les paramètres et créer des variables locales.

enum variant_type 
{ 
   Symbol, Number, List, Proc, Lambda, Chaine 
}; 
 
// definition à venir ; Variant et Environment se référencent mutuellement 
struct Environment;  
 
// un Variant représente tout type de valeur Lisp 
class Variant { 
public: 
 
   // fonction qui renvoie Variant et qui prend comme argument variants 
 
   typedef Variant(*proc_type) ( const std::vector<Variant>& ); 
 
   typedef std::vector<Variant>::const_iterator iter; 
    
   typedef std::map<std::string, Variant> map; 
 
   // types pris dans l'énumération : symbol, number, list, proc ou lamda 
   variant_type type;   
        
   // valeur scalaire 
   std::string val;     
        
   // valeur list 
   std::vector<Variant> list;  
 
   // valeur lambda 
   proc_type proc;      
    
   // environnement 
   Environment* env; 
    
   // constructeurs 
   Variant(variant_type type = Symbol) : type(type) , env(0), proc(0) { 
 
   } 
 
   Variant(variant_type type, const std::string& val) :  
type(type), val(val) , env(0) , proc(0) { 
    
   } 
    
   Variant(proc_type proc) : type(Proc), proc(proc)...