[PDF] Cours langage C++ : classes, Héritage et Polymorphisme | Formation informatique

75  Download (0)

Texte intégral

(1)

Thierry Dumont

Institut Camille Jordan.

C++ EN CALCUL SCIENTIFIQUE.

ÉCOLE DOCTORALE INFOMATHS,

NOTES DE COURS.

(2)

Thierry Dumont, Institut Camille Jordan.

(3)

C++ EN CALCUL SCIENTIFIQUE.

ÉCOLE DOCTORALE INFOMATHS,

NOTES DE COURS.

Thierry Dumont

Institut Camille Jordan.

(4)
(5)

TABLE DES MATIÈRES

1. Introduction : pourquoi programmer en C++, en Calcul Scientifique . . . . 9

1.1. Abstraction . . . 9

1.2. Généricité . . . 10

Le meilleur et le pire des langages . . . 10

1.3. Pourquoi ne pas programmer en C++. . . 10

2. Introduction aux classes . . . 11

2.1. De C à C++ . . . 11

2.2. Les opérateurs surchargeables . . . 19

2.3. La classe Array (presque) complète et son utilisation . . . 21

2.4. Remarques finales importantes . . . 23

2.5. Retour sur le passage de paramètres . . . 23

2.6. Deux présentations possibles . . . 24

3. Héritage & Polymorphisme . . . 25

3.1. Héritage . . . 25

3.2. Polymorphisme . . . 28

4. Interfaçage (application à la résolution de systèmes linéaires) . . . 33

Règles : . . . 33

La méthode factorize de la classeSquareMatrix: . . . 34

Résolution de systèmes linéaires . . . 35

5. Généricité . . . 39

5.1. Niveau 1 : templates . . . 39

5.2. Niveau 2 : une courte introduction à la Standard Template Library (STL) . . . 41

5.3. Application à la construction de matrices creuses . . . 45

5.4. Le traitement des callback . . . 46

6. Objets fonctions . . . 49

6.1. Introduction . . . 49

6.2. La STL et les objets fonctions . . . 50

(6)

A. Arbres équilibrés . . . 61

B. Tri : la méthode du Quicksort . . . 63

C. Matrices creuses . . . 65

D. Documenter un code C++ : Doxygen . . . 69

E. Évolution du langage C++ . . . 71

E.1. L’avant garde : boost . . . 71

Index . . . 73

(7)

Attention !

Ces notes de cours ne peuvent en aucune manière remplacer un manuel de référence de C++. Un grand nombre d’aspects et de notions importants ne sont pas décrits ; par exemple :

– les entrées sorties, – les exceptions

– le langage C et ses structures de contrôle. – etc.

Le lecteur est invité à rechercher ce qui manque dans les nombreuses références disponibles, livres ou ressources en ligne.

La référence absolue est le livre de Bjarne Stroustrup l’« inventeur »du langage C++ [Str97], dont le volume est impressionnant, mais qui n’est pas dénué d’aspects pédagogiques (mais pas du tout orientés calcul). Le polycopié de Todd Veldhuizen, bien qu’ancien, est -lui- axé vers les applications numériques [Vel00] ; c’est une introduction aux aspects les plus sophistiqués du C++ utilisés en Calcul Scientifique (meta programmes, template d’expressions) ; attention c’est une lecture difficile. Pour une étude systématique des éléments de programmation avancés le livre [GHJV95] est très utile.

J’ai ajouté en annexe la description des arbres équilibrés et de l’algorithme du Quicksort qui ne sont bien sûr pas du C++, mais qui sont utilisés par la STL.

(8)
(9)

CHAPITRE 1

INTRODUCTION : POURQUOI PROGRAMMER EN C++, EN CALCUL

SCIENTIFIQUE

Alexander Alexandrovich Stepanov (auteur avec A. Lee de la Standard Template Library –dont il sera question plus loin–) considère le langage C comme un modèle d’ordinateur. C est un langage de relativement bas niveau, au-dessus duquel on construit un langage de haut niveau : C++.

C++ rajoute des couches d’abstraction à C :

– la programmation orientée objets (classes, héritage, etc.)

– la programmation générique (patrons (template) de programmes).

C++ va donc permettre de faire tout ce que fait C et permet potentiellement d’atteindre les performances optimales de l’ordinateur, tout en ayant le niveau d’abstraction souhaité.

1.1. Abstraction

Pour nous, il s’agit de cacher de l’information, ce qui est le plus familier aux mathématiciens : les

mots deπ, Groupe, Corps, Graphe etc. sont des abstractions qui cachent des définitions précises ;

de même qu’on peut manipuler tous ces objets sans référencer en permanence leur définition, l’abstraction des langages à objets permet de manipuler des objets « informatiques »sans référencer leur représentation machine.

Il y a bien longtemps, N. Wirth a introduit la maxime :

Algorithme + Structure de données = Programme.

Pour un programmeur scientifique, les performances des codes de calcul sont fondamentales ; la maxime de Wirth dit qu’un bon algorithme ne peut donner un bon programme que si une structure de données adaptée à l’algorithme est implantée. À priori, cela va entraîner une grande rigidité des programmes, et c’est bien ce dont souffraient les codes d’avant la programmation objet : êtes vous sûr de pouvoir définir la bonne structure de données à priori ? Et puis votre algorithme peut évoluer. Pensez par exemple à un maillage éléments finis : votre structure ne stocke que des simplexes puis, un jour, vous avez besoin d’hexaèdres : il y a de fortes chances pour que vous deviez réécrire le code de votre algorithme, si tout est couplé. De plus , avec les machines actuelles, algorithmes et structures de données doivent composer avec des contraintes matérielles comme la lenteur de l’accès à la mémoire, qui est lent. Cacher la représentation interne des structures de données permet un développement sûr, mais aussi d’expérimenter facilement.

(10)

1.2. Généricité

Prenons un exemple, volontairement simple. Soit E un ensemble fini muni d’une relation d’ordre total < : tous les algorithmes de tri reposent sur la disponibilité de deux opérations fondamentales, et n’en nécessitent aucune d’autre :

1. ∀ei∈ E , ∀ej∈ E, la comparaison ei< ejest calculable (et vaut vrai au faux),

2. on peut permuter deux éléments quelconques eiet ej∈ E.

On peut donc espérer d’un langage de programmation que, pour chaque algorithme de tri que l’on souhaite implanter, il n’y ait besoin que d’une seule implantation, qui puisse être suffisamment versatile pour se spécialiser à la donnée de tout ensemble E vérifiant les propriétés ci-dessus énoncées.

Le meilleur et le pire des langages

Comme la langue d’Ésope, C++ est la meilleure et la pire des choses (en calcul scientifique, en tout cas) :

– la meilleure : théoriquement, se baser sur un langage de bas niveau (C, le fameux modèle d’ordinateur) permet d’accéder aux meilleurs performances ; on verra que dans la réalité on doit nuancer ce propos.

– la pire :

– Le problème avec C++ en calcul scientifique est qu’un certain nombre de constructions, très séduisantes à priori, ne doivent pas être utilisées sous peine d’une baisse intolé-rable des performances. Un cours adapté de C++ adapté au calcul est nécessaire ! Le programmeur C++ doit faire le grand écart : l’abstraction permet de s’éloigner beaucoup du hardware, mais il faut aussi regarder les programmes du point de vue de leur adaptation au hardware : cela nécessite le développement d’une certaine manière de penser. – C++ est un langage compliqué, relativement difficile à apprendre.

– C++ est construit sur C : il hérité donc des principaux défauts de C, comme par exemple le très bas niveau de l’implantation de l’allocation dynamique, ce qui peut rend facilement les programmes peu sûrs.

1.3. Pourquoi ne pas programmer en C++.

Il y a bien sûr les arguments avancés par les membres de diverses sectes : la secte Java, les sectes Fortran, la secte Python, ou même la secte Ocaml... Même s’il est relativement sans intérêt de polémiquer avec leurs zélateurs, on peut faire remarquer 1) que Java, Python etc. ne résolvent pas les mêmes problèmes : on ne codera pas une transformée de Fourier rapide performante dans ces langages ; 2) Fortran est certes très utilisé en Calcul Scientifique (c’est historique), mais les versions actuelles de ce langage sont à peu près aussi difficiles à apprendre que le C++.

Mais avez vous besoin de C++ (ou de Fortran) ? Pas sûr... Si vous n’avez besoin que d’assembler des algorithmes existants, pour lesquels il existe des bibliothèques (écrites, elles, en C, en C++, en Fortran ; mais vous n’avez pas besoin de rentrer dans ces détails), bref si votre code va passer le plus clair de son temps dans des routines préexistantes, alors assemblez ces routines avec un langage comme Python : le développement sera plus simple et plus rapide, et les performances seront respectables.

(11)

CHAPITRE 2

INTRODUCTION AUX CLASSES

Les classes sont à la base de toute la construction objet de C++.

On va construire progressivement une classe simple mais utile : une classe de tableaux (Array).

2.1. De C à C++

Rappelons que, en C, tableaux et pointeurs sont un peu plus que frères.

En C, un vecteurX(= un tableau à une seule dimension) est représenté par un pointeur (c’estX),

auquel est accrochée la quantité de mémoire nécessaire (ici, on utilise déjà ici une syntaxe C++,

mais pour écrire du C :newetdelete, c’est du C++) :

1 double *X ; 2 X=new double [ 5 0 ] ; 3 for ( i n t i =0; i <50; i ++) 4 X[ i ]= i ; 5 delete [ ] X ; Remarques : – 

Toute variable doit être déclarée, mais attention :

double* X,Y: déclareXcomme un pointeur sur un double,Ycomme un double. Il faut

écrire :double *X,*Ysi on a 2 pointeurs à déclarer !

– 

Ici, les indices vont de zéro à (49, inclus, ici). C’est bien pratique pour représenter des polynômes, moins pour des matrices.

– 

delete[]: les crochets sont nécessaires quand un vecteur est associé à un pointeur !

delete X serait une faute ici.

– 

Tout ce qui est alloué doit être détruit ! On ne doit réutiliser un pointeur que quand ce sur quoi il point a été désalloué.

Les tableaux à 2 indices sont accessibles en C par des expressions commeX[i][j], ce qui implique

ici queXest undouble **, et donc queX[i]est un pointeur sur undouble, ce qui est peu

(12)

– quelles sont les performances de cette représentation des tableaux ?

– Pour l’appel de procédure, si la procédure –cas fréquent– doit connaître les bornes du tableau, on va devoir les passer comme argument ; exemple :

1 double sumMyCoeffs ( i n t n , i n t m, double X [ ] [ ] )

2 { 3 double r e t = 0 . 0 ; 4 for ( i n t i =0; i <n ; i ++) 5 for ( i n t j =0; j <m; j ++) 6 r e t +=X [ i ] [ j ] ; 7 return r e t ; 8 }

2.1.1. Un peu de cohérence : les « struct » C. — C’est un petit pas vers les classes de C++ :

1 s t r u c t Array

2 {

3 i n t n ,m;

4 double ** x ;

5 } ;

On pourra instancier un tableau de la manière suivante :

1 Array Y ; / / mon tableau s ’ appelle Y

2 Y . n=10; Y .m=20;

3 Y . x=new double * [ Y . n ] ;

4 for ( i n t i =0; i <Y .m; i ++)

5 Y . x [ i ]=new double [ Y .m] ;

Ce qui permettra de réécrire la procédure ci-dessus : 1 double sumMyCoeffs ( Array T)

2 { 3 double r e t = 0 . 0 ; 4 for ( i n t i =0; i <T . n ; i ++) 5 for ( i n t j =0; j <T .m; j ++) 6 r e t +=X [ i ] [ j ] ; 7 return r e t ; 8 }

On est certes devenus un peu plus cohérents, mais c’est bien insuffisant. On remarquera aussi la

lourdeur de l’allocation du tableau(1). Autre problème : il n’y a aucune garantie pour que le tableau

occupe un espace contigu en mémoire => risque de performances très médiocres.

Le cahier des charges minimal d’unArraypourrait être :

1. On alloue d’abord un tableau de pointeurs, puis des vecteurs ; la désallocation doit être faire exactement en sens inverse

(13)

1. Cacher complètement la représentation interne : on doit pouvoir créer, supprimer unArray

sans accéder explicitement à sa représentation. 2. On veut de bonnes performances.

3. On veut pouvoir passer ce tableau à des routines écrites dans d’autres langages (Fortran). 4. On veut que les indices puissent commencer à d’autres bornes que zéro.

5. On veut pouvoir tester et attraper (catch) les accès illicites en dehors des bornes des tableaux. 6. On veut que toutes les opérations puissent être faites sans connaître la représentation interne

desArray.

7. On veut pouvoir faire plein d’opérations élémentaires sur les tableaux.

La notion de classe et son implantation en C++ (class) permettent de satisfaire ce cahier des

charges.

On part desstructdu C, que l’on va amplifier, pour obtenir les classes du C++.

2.1.2. Première étape : les « struct »avec méthodes. — On ajoute auxstructdu C des

« fonctions », appelées ici méthodes pour manipuler lesArray, mais aussi les créer et les détruire.

1 s t r u c t Array 2 { 3 i n t n ,m; 4 double * x ; 5 / / c on s t r u c t e u r 6 Array ( i n t N, i n t M) 7 { 8 n=N; m=M; 9 x=new double [ n*m] ; 10 } 11 / / d e s t r u c t e u r . 12 ~Array ( ) 13 { 14 delete [ ] x ; 15 } 16 / / somme des c o e f f i c i e n t s 17 double sumMyCoeffs ( ) 18 { 19 r e t = 0 . ; 20 / / comment e c r i r e ce qui s u i t ? 21 22 return r e t ; 23 } 24 } ;

Comment utiliser ces Array ?

Il faut d’abord que toute unité de programme qui utilise lesArrayait suffisamment de

connaissances pour les utiliser ; si la classeArrayest stockée dans un fichier de nomArray.hpp

(ce qui est une convention possible), l’unité de programme devra inclure ce fichier. Voici un exemple de création (et de destruction :

(14)

1 #include ‘ ‘ Array . hpp ’ ’ 2 i n t main ( ) 3 { 4 Array *p=0; 5 Array X ( 2 0 , 1 0 ) ; 6 7 / / quelques i n s t r u c t i o n s u t i l i s a n t X 8 9 p=new Array ( 2 0 , 3 0 ) ; 10 11 / / quelques i n s t r u c t i o n s u t i l i s a n t p 12 13 delete p ; 14 }

À la ligne 5, on crée unArray: c’est le constructeur (ligne 6 de la structureArray) qui est appelé.

Quand le destructeur est il appelé ? Au moment ou l’objetXsort de visibilité, c’est à dire ici à la fin

du programme :Xest une variable automatique. À la ligne 9, on alloue un tableau, pointé parp, et

on le désalloue ligne 13.

 Les objets alloués par new doivent être explicitement détruits par delete.

 Pour détruire un objet alloué parnew, on utilise l’ordredelete, pour détruire un tableau

d’objets alloués parnew, on utilisedelete[].

Bonne pratique : initialiser les pointeurs à zéro (ligne 4). Les variables ne sont pas initialisées à une quelconque valeur par défaut en C++. L’initialisation à zéro des pointeurs permet de repérer plus facilement des pointeurs non alloués (deboguage)

Nous avons un peu progressé. Mais on est loin du cahier des charges énoncé plus haut.

Commençons par sécuriser notre classeArrayavec les mots clésprivateetpublic(voir le listing

page 15.

Ce qui est après le motpublicest accessible de l’extérieur, par exemple depuismain. Ce qui est

privaten’est accessible que par les objets de la classeArray(2)(noter ici queprivateest

facultatif : ce qui n’est pas déclaré explicitement estprivate. Unestructest uneclassoù tout

(méthodes et membres) est publique. Nous verrons plus loin l’existence d’un autre attribut :

protected.

Notons que peuvent êtrepublicouprivateaussi bien des membres que des méthodes (et

d’autres choses aussi, on le verra plus tard).

2.1.2.0.1. Organisation du tableau. — Quelque chose a changé dans notre implantation : on alloue

un seul vecteurx: notre tableau bidimensionnel va être stocké en entier dans ce vecteur : on

garantit ainsi que le tableau utilisera des adresses contiguës en mémoire ; c’est aussi indispensable pour permettre l’interfaçage avec d’autres langages (comme Fortran).

La technique consiste donc à stocker un tableau à 2 indices dans un vecteur (ce qui est généralisable à plus de 2 indices). Il y a deux possibilités pour cela :

1. soit on range les lignes les unes après les autres de 0 à m − 1 dans le vecteurX[],

(15)

1 c l a s s Array 2 { 3 private : 4 i n t n ,m; 5 double * x ; 6 public : 7 / / c on s t r u c t e u r 8 Array ( i n t N, i n t M) 9 { 10 n=N; m=M; 11 x=new double [ n*m] ; 12 } 13 / / d e s t r u c t e u r . 14 ~Array ( ) 15 { 16 delete [ ] x ; 17 } 18 / / somme des c o e f f i c i e n t s 19 double sumMyCoeffs ( ) 20 { 21 r e t = 0 . ; 22 / / comment e c r i r e ce qui s u i t ? 23 24 return r e t ; 25 } 26 } ;

FIGURE1. Introduction de public et private.

2. soit on range les colonnes les unes après les autres, de 0 à n − 1.

 Remarques très importantes :

1. Le choix de l’ordre de stockage est très important : dès que le tableau est trop grand pour rentrer dans le cache machine, il faut absolument que les algorithmes utilisant le tableau le parcourent par adresses conjointes ;

2. le rangement colonne après colonne est celui du Fortran : c’est celui qu’il faut utiliser si on veut interfacer son code avec des routines écrites dans ce langage (algèbre linéaire via Lapack, par exemple.

2.1.2.0.2. L’accès aux composantes du tableau. — On suppose pour l’instant que les indices commencent à zéro.

1. Rangement ligne après ligne : la correspondance est : (i , j ) → i ∗ m + j. 2. Rangement colonne après colonne :

(16)

On adapte facilement ces formules pour des indices ne commençant pas à zéro.

2.1.2.0.3. La réalisation pratique : premiers opérateurs en C++.— On veut pouvoir écrire :

double z= X(2,3);

On va utiliser la possibilité en C++ de surcharger les opérateurs, et il se trouve que()est un

opérateur ; rajoutons à la partiepublicde notre classe, la méthode suivante (correspond au

premier cas) :

i n l i n e double operator ( ) ( i n t i , i n t j ) const

{

return X [ j *m+ i ] ;

}

On définit ici l’opérateur(); la syntaxe est imposée : pour définir l’opérateur(), il faut écrire la

méthodeoperator()(...): le nom de la méthode est fixé, mais pas le type ni le nombre de ses

arguments. L’instruction

double z= X(2,3);

va provoquer l’appel de la méthode operator()(2,3).

Attention, ici, tout est important !

– inline: dit au compilateur de remplacer un appel de fonction par une recopie en ligne de celle ci, partout où elle apparaît.

La taille et la complexité des méthodes que le compilateur peut « inliner »est limitée. L’impact sur les performances, pour de petites fonctions (comme ici) peut être très important : un branchement entraîne une rupture du pipeline.. et mauvaises performances. Si le compilateur réussit à inliner la méthode, il n’y a pas de branchement.

– constindique au compilateur que les membres de l’objetArray(ici :m,netX) ne sont pas modifiés par la fonction ; là aussi il y a un gain possible en performances mais aussi en sécurité :

si une méthode déclaréeconstcherche à modifier les membres, c’est une erreur que le

compilateur détectera.

Bonnes pratiques :

– multiplier les méthodes simples, en prenant soin à leur écriture. – utiliser const et inline.

Ok. Maintenant dansmain()je peux écrire :

double z= X(2,3);

mais je ne peux pas écrire :

X(2,3)=1.;

Il y a plusieurs raisons à cela :

1. le mot cléconstdans la définition de l’opérateur(). Évidemment, il empêche de modifier les

valeurs deX. Mais on pourra vérifier qu’on ne s’en sort pas en le supprimant.

2. Que fait operator() ? comme toute fonction nonvoidil copie le résultat (instructionreturn)

sur la pile. L’instructionz= X(2,3)recopie le dessus de la pile dansz. On voit donc bien que

(17)

Pour pouvoir écrireX(2,3)=1, il faut que l’opérateur (cad. la méthodeoperator()) renvoie un

moyen ad’hoc d’accéder à la composante (2, 3) deX.x. On peut penser à un pointeur, mais ce serait

peut pratique et très inélégant.

C++ fournit pour cela la notion de référence.

2.1.2.0.4. Une référence est un alias pour une variable. — Exemples :

double z ;

double& z r e f =z ; / / z r e f e s t une r e f e r e n c e , qui designe z .

z =1; / / z r e f e t z valent 1 .

i n t x [ 1 0 ] ;

i n t& y=x [ 5 ] ; / / y designe x [ 5 ]

Bien noter qu’une référence réfère toujours quelque chose ; les instructions suivantes sont illégales :

Array X ( 3 , 4 ) ; Array& Y ;

On verra plus tard l’intérêt des références pour le passage de paramètres.

Derrière la scène, il y a des pointeurs (ce n’est pas explicite dans la norme C++, mais c’est implanté ainsi : les références cachent les lourdes et pénibles écritures à l’aide de pointeurs).

Revenons à notre opérateur :

i n l i n e double& operator ( ) ( i n t i , i n t j )

{

return X [ j *m+ i ] ;

}

On a évidemment suppriméconst, et on renvoi une référence à X[j*m+i]; on peut l’utiliser de

deux manières :

X(2,3)=1.;

double z= X(2,3);

Mais on peut conserver les deux méthodes pour l’opérateur(): en principe le compilateur choisira

la bonne méthode ; il y a quelque chance pour que la première soit plus rapide, quand elle peut être utilisée. En C++ une classe peut avoir plusieurs méthodes de même nom, mais qui diffèrent par leur signature (les paramètres, le type du résultat).

2.1.2.0.5. Debogage : comment tester et piéger les débordements de tableau, les indices incorrects ?— Nous sommes maintenant maîtres de l’accès aux composantes de notre tableau. Réécrivons un peu notre (nos) opérateur(s) :

(18)

i n l i n e double& operator ( ) ( i n t i , i n t j )

{

# i f d e f DEBUG

i f ( i < 0 | | i >=n | | j < 0 | | j >=m)

throw MyException ( ‘ ‘ Array : i ndi c es out of bounds ’ ’ ,n ,m, i , j ) ; #endif

return X [ j *m+ i ] ;

}

Explications :

– #ifdef DEBUG...#endif: ce bloc est traité par le préprocesseur, comme les ordres#include,

avant la compilation. SiDEBUGest définit, les lignes du bloc sont insérées dans le texte et donc

compilées, sinon elles sont supprimées.

Comment définirDEBUG? Il faut soit ajouter une ligne#define DEBUGavant l’inclusion du

fichier concerné (par exemple en tête dumain, ou sur la ligne de commande du compilateur, en

ajoutant-DDEBUG.

– throw MyException:

on « lance »une exception : pour ce faire on instantie un objet d’un type donné, ici de la classe

MyException.

Voici un exemple de classe, qui peut être utilisé ici :

c l a s s MyException

{

public :

MyException ( std : : s t r i n g s , i n t n , i n t m, i n t i , i n t j ) {

std : : cout<<s<< ’ ’ <<n<< ’ ’ <<m<< ’ ’ <<i << ’ ’ << j <<std : : endl ; }

} ;

L’instantiation produira un message, puis un plantage du programme ... on peut faire plus sophistiqué :

' SE REPORTER À UN MANUELC++POUR LA DESCRIPTION DE CE MÉCANISME,BEAUCOUP PLUS RICHE QUE CE QUI EST ÉCRIT ICI.

 Ce mécanisme ne doit être utilisé que pour ce pour quoi il a été conçu : la gestion

d’exceptions : son exécution est lente ! ne pas s’en servir pour sortir de boucles, par exemple !

Notre classe de tableaux commence a pouvoir être utilisée. Enrichissons-là de quelques méthodes. 2.1.2.0.6. Initialisations. — Remarquons d’abord qu’une classe peut avoir plusieurs constructeurs (mais un seul destructeur). On peut faire des constructeurs qui diffèrent par le nombre (≥ 0) et le

(19)

Array ( const Array& A) { n=A . n ; m=A .m; x=new double [ n*m] ; for ( i n t i =0; i <n*m; i ++) x [ i ]=A . x [ i ] ; } }

On construit donc un objet à partir d’un autre objet du même type.

Les paramètres de ce constructeur ne peuvent pas être quelconques : en plus de celui utilisé ici on

peut écrire aussiArray(Array& A), mais rien d’autre.

Quelques explications :

– on passe une référence à un objet de typeArray.

– même remarque que précédemment à propos deconst: le compilateur va tester si l’objetAest

modifié dans le constructeur.



Le compilateur fournit uncopy constructorpar défaut, qui est équivalent à :

Array ( const Array& A) { n=A . n ; m=A .m; x=A . x ; } } 

 Danger ! Les deux tableaux partagent les mêmes données dansx[]!

Une bonne pratique : toujours munir une classe de son copy-constructor.

2.2. Les opérateurs surchargeables

On peut donc étendre la définition de certains opérateurs aux objets que nous créons. La liste des opérateurs surchargeables est disponible par exemple ici :

http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B. Définissons quelques opérateurs utiles pour notre classe de tableau. – operator=.

À priori, le sens est différent du copy constructor. Voilà une réalisation possible : 1 i n l i n e void operator =( const Array& A)

2 { 3 i f (A . n! =n | | A .m! =m) 4 throw MyException ( . . . . ) ; 5 e l s e 6 for ( i n t i =0; i <m*n ; i ++) 7 x [ i ]=A . x [ i ] ; 8 }

(20)

Remarques :

1. l’opérateur ne renvoi rien : il est donc void.

2. il semble raisonnable de déclarer les paramètres comme dans le copy constructor.

3. on peut avantageusement remplacer les lignes 6 et 7 par un appel à l’algorithmecopyde la

stl(page 44).

4. On pourait donner un sens complètement différent à cet opérateur : sans aller chercher des choses extrêmes, on pourrait admettre que, si les dimensions ne coïncident pas, le tableau cible est détruit et recréé.

 Il n’y a aucune sémantique préétablie pour les opérateurs en C++ !

Ceci a des avantages (souplesse) et des inconvénients : parmi les plus subtils, les

opérateurs ne définissent pas de structures algébriques :operator+()n’est pas associatif

par exemple : le compilateur ne peut donc pas se servir de propriétés algébriques pour optimiser.

– operator+=

Voici deux exemples, à priori évidents, deux méthodes de même nom qui diffèrent par leurs signatures :

i n l i n e void operator +=( const Array& A)

{ i f (A . n! =n | | A .m! =m) throw MyException ( . . . . ) ; e l s e for ( i n t i =0; i <m*n ; i ++) x [ i ]+=A . x [ i ] ; }

LISTING2.1. Une première version de la classe Array.

i n l i n e void operator +=(double v )

{

for ( i n t i =0; i <m*n ; i ++)

x [ i ]+= v ; }

On pourrait aussi définir des opérateursoperator*=,operator/=,operator-=ou

operator=(double v).

Remarque sur le passage de paramètres : copies ou références ?

Considérons les procédures :

i n l i n e void doSomething ( const Array& A)

{

/ / . . . . . }

(21)

i n l i n e void doSomething ( Array A)

{

/ / . . . . . }

– Dans le premier cas, on passe une référence : en pratique, lors de l’appel on copie sur la pile un pointeur vers l’objet passé en paramètre ; dans le second cas (passage par valeur), on copie sur la pile l’objet lui-même : c’est à dire les 3 variables m, n et x.

– Passer un objet par valeur (c’est le deuxième cas) peut amener à faire des copies de tailles prohibitives sur la pile, au détriment des performances. Contrairement à ce qu’on pourrait penser, ça ne sécurise pas l’objet passé en paramètre : on a une copie de x, et on a donc accès au tableau pointé par x, en lecture et en écriture.

L’inconvénient de la première méthode est qu’elle utilise en interne des pointeurs, ce qui peut malgré tout avoir un coût. C’est malgré tout la seule raisonnable avec des objets un peu gros.

2.3. La classe Array (presque) complète et son utilisation

1 # i f n d e f Array__h 2 #define Array__h 3 #include "MyException . hpp" 4 c l a s s Array 5 { 6 private : 7 i n t n ,m; 8 double * x ; 9 public : 10 / / c o n s t r u c t e u r 11 Array ( i n t N, i n t M) 12 { 13 n=N; m=M; 14 x=new double [ n*m] ; 15 } 16 / / d e s t r u c t e u r . 17 ~Array ( ) 18 { 19 delete [ ] x ; 20 }

21 Array ( const Array& A)

22 { 23 n=A . n ; m=A .m; 24 x=new double [ n*m] ; 25 for ( i n t i =0; i <n*m; i ++) 26 x [ i ]=A . x [ i ] ; 27 }

28 i n l i n e void operator =( const Array& A)

29 {

30 i f (A . n! =n | | A .m! =m)

31 throw MyException ( " Array : : operator = : dimensions d i f f e r " ) ;

32 e l s e

33 for ( i n t i =0; i <m*n ; i ++) 34 x [ i ]=A . x [ i ] ;

35 }

36 i n l i n e void operator =( double v )

37 {

38 for ( i n t i =0; i <m*n ; i ++) 39 x [ i ]= v ;

40 }

41 i n l i n e void operator +=( const Array& A)

42 {

43 i f (A . n! =n | | A .m! =m)

44 throw MyException (

45 " Array : : operator +=( const Array& A ) , dimensions d i f f e r " ) ; 46 e l s e

(22)

48 x [ i ]+=A . x [ i ] ;

49 }

50 i n l i n e void operator +=(double v )

51 {

52 for ( i n t i =0; i <m*n ; i ++)

53 x [ i ]+=v ;

54 }

55

56 i n l i n e double operator ( ) ( i n t i , i n t j ) const

57 {

58 # i f d e f DEBUG

59 i f ( i < 0 | | i >=n | | j < 0 | | j >=m)

60 throw MyException ( " Array : i nd ic es out of bounds" ,n ,m, i , j ) ;

61 #endif 62 return x [ j *m+ i ] ; 63 } 64 i n l i n e double& operator ( ) ( i n t i , i n t j ) 65 { 66 # i f d e f DEBUG 67 i f ( i < 0 | | i >=n | | j < 0 | | j >=m)

68 throw MyException ( " Array : i nd ic es out of bounds" ,n ,m, i , j ) ;

69 #endif

70 return x [ j *m+ i ] ;

71 }

72 / / somme des c o e f f i c i e n t s 73 i n l i n e double sumMyCoeffs ( ) const

74 { 75 double r e t = 0 . ; 76 for ( i n t i =0; i <n*m; i ++) 77 r e t +=x [ i ] ; 78 return r e t ; 79 } 80 81 i n l i n e i n t n l i g ( ) const { return n ; }

82 i n l i n e i n t ncol ( ) const { return m; }

83 } ; 84 #endif

Et voici un programme utilisant des Array : 1 #include " Array . hpp" 2 #include <iostream > 3 i n t main ( ) 4 { 5 using namespace std ; 6 Array X ( 1 0 , 5 ) ; 7 Array Y (X ) ; 8 Array Z=X ; 9 i n t n=X . n l i g ( ) ,m=X . ncol ( ) ; 10 for ( i n t i =0; i <n ; i ++) 11 for ( i n t j =0; j <m; j ++) 12 X( i , j ) = 1 . / ( 1 + i + j ) ; 13 14 Y=1; 15 16 X+=Y ; 17 18 X+=2; 19 20 X(0 ,0)= −100; 21 22 for ( i n t i =0; i <n ; i ++) 23 { 24 for ( i n t j =0; j <m; j ++) 25 cout<<X( i , j )<< ’ ’ ; 26 cout<<endl ; 27 } 28 cout<<"−−−end−−−"<<endl ; 29 } Quelques commentaires :

(23)

# i f n d e f Array__h #define Array__h

#endif

le but est de ne pas inclure plusieurs fois ce source, ce qui produirait une erreur à la compilation.

2. il n’y a pas de commentaires : c’est mal ! On verra comment écrire et traiter des commentaires utiles (cf. page 69).

3. les opérateurs commeoperator-=ne sont pas codés, mais il se déduisent facilement des

opérateursoperator+=.

4.  Que fait l’instructionArray Z=X(ligne 8) ? Quelle méthode est appelée ? Réponse : le

copy constructor. Attention donc à ce que fait vraiment cette méthode...

2.4. Remarques finales importantes

On aimerait pouvoir écrire des expressions comme celles-ci : 1 Array X( 2 0 , 3 0 ) , Y ( 2 0 , 3 0 ) ,Z ( 2 0 , 3 0 ) ;

2 / / i n s t r u c t i o n s u t i l i s a n t X e t Y :

3 . . . . .

4 . . . . .

5 Z=2*X+Y ;

Une programmation naïve est possible, mais complètement inefficace : elle reviendrait à peu près, pour la ligne 5 à :

1. allouer un nouveau tableau, appelons le T1, pour stocker 2 ∗ X ,

2. calculer T1+ Y et stocker le résultat dans un nouveau tableau T2.

3. copier T2dans Z ,

ce qui est à comparer avec une programmation traditionnelle (en C ou en Fortran) où une simple boucle suffit pour coder Z=2*X+Y. La création de 2 tableaux temporaires et les copies ont un coût prohibitif.

Une solution existe, qui permet d’utiliser la syntaxe de la ligne 5, en pur C++, et d’obtenir

exactement le codage classique (une boucle) : la technique desexpression templates. Elle est

largement au-delà des limites de ce cours

(voirhttp://en.wikipedia.org/wiki/Expression_templateset\cite{veld}et les

références). La bibliothèqueBlitz++implante très efficacement ces techniques.

2.5. Retour sur le passage de paramètres

On reprend le calcul de la somme des coefficients de nos tableaux (Array). Au lieu d’écrire une

méthode, on peut écrire une fonction externe, ce qui implique de passer un objet de typeArrayen

paramètre.

(24)

double sumcoeffs ( const Array& A)

{

double r e t = 0 . 0 ;

i n t n=A . n l i g ( ) ,m=A . ncol ( ) ; for ( i n t i =0; i <n ; i ++)

for ( i n t j =0; j <m; j ++)

r e t +=A( i , j ) ;

return r e t ;

}

Ici on passe une référence (déclaréeconst). En pratique, c’est un pointeur qui va être passé.

Changeons le paramètre :

double sumcoeffs ( Array A)

{ . . . }

Ici le paramètre est passé par valeur, c’est à dire que le paramètre effectif sera recopié sur la pile. Et comment est-il recopié ? En appelant le copy constructeur (on peut s’en convaincre en glissant un ordre d’impression dans le copy constructeur).

 Ceci potentiellement dangereux : le risque de faire des copies de gros objets est important en

C++ ! Ce qui est évidemment désastreux pour les performances. Notons qu’il est possible de rendre les objets d’une classe non copiables.

2.6. Deux présentations possibles

Comment concrètement réaliser tout cela ? Il y a au moins deux solutions :

1. Créer trois fichiers :Array.hpp, MyException.hpp, main.cpp. Sous Linux, on pourra

compiler le code avec une ligne de commande comme :

g++ main.cpp -o exec

pour obtenir un programme exécutableexec.

Pour activer le flag DEBUG la ligne deviendra :

g++ main.cpp -o exec -DDEBUG

On pourra activer l’option debug (la possibilité d’utiliser un debogueur externe en ajoutant

l’option-g; pour une optimisation sérieuse, il faut que le compilateur optimise et on ajoutera

par exemple-O3. L’option-Wallpermet de voir tous les diagnostiques du compilateur :

l’utiliser est recommandé.

2. On peut scinderArray.hppen 2 fichiers, l’unArray.cppqui contiendra les instructions et

l’autreArray.hppqui ne contiendra que les définitions. Cela permet une compilation

séparée de la classe Array et des programmes qui l’utilisent (ici : main.cpp), et la création de bibliothèques (library).

(25)

CHAPITRE 3

HÉRITAGE & POLYMORPHISME

On est là au cœur de la programmation orientée objets. La notion d’héritage est relativement simple et utile ; celle de polymorphisme aussi, mais l’implantation du polymorphisme n’est pas sans poser des problèmes de performances en calcul scientifique.

3.1. Héritage

On va donner un exemple, basé sur le chapitre précédent.

Nous voulons construire deux classes : les matrices carrées et les vecteurs. Il parait raisonnable de récupérer ce qui a été fait, d’autant plus que les matrices carrées apparaissent naturellement comme un spécialisation des tableaux, et de même pour les vecteurs. Si on veut faire de l’algèbre linéaire, il parait normal que ces deux nouveaux types d’objets partagent des structures et des méthodes.

3.1.1. Les matrices symétriques. — Voici une ébauche de classe :

1 # i f n d e f SquareMatrix__h

2 #define SquareMatrix__h

3 #include " Array . hpp"

4

5 c l a s s SquareMatrix : public Array

6 { 7 bool f a c t o r i z e d ; 8 9 public : 10 11 / / ! c o n s t r u c t e u r 12 / / ! \param N t a i l l e . 13 SquareMatrix ( i n t N) : Array (N,N) 14 { 15 f a c t o r i z e d = f a l s e ; 16 } 17 / / ! d e s t r u c t e u r 18 ~SquareMatrix ( ) { } 19 } ;

(26)

– à la ligne 3, on inclue les informations nécessaires.

– à la ligne 5, on définit la classeSquareMatrixcomme dérivant de la classeArray.

– public Array: tout champ public de la classeArraydevient un champ public de la classe

SquareMatrix. Il est tout à fait possible, mais pas très souvent utile de mettreprivate, à la

place depublic.

– On peut bien sûr rajouter des champs (et des méthodes) dans la classe dérivée : ici

SquareMatrixa un champfactorized(qui donnera l’état de la matrice : matrice telle qu’on l’a créée, ou bien qui a subit une factorisation LU – on verra ça plus loin –).

– Le constructeurSquareMatrix(int N)appelle le constructeur de la classe de baseArray: c’est

obligatoire (Arraypeut avoir plusieurs constructeurs, et il faut forcément en choisir un).

 L’appel d’un constructeur de la classe de base est obligatoire(1).

– Pas de problème ni de précaution pour le destructeur, qui est unique pour chaque classe : le compilateur fait le travail tout seul, et il suffit d’appeler le destructeur de la classe dérivée.

Mais il faut modifier la classeArray: c’est là qu’intervient le mot cléprotected: il permet l’accès

aux champs et aux méthodes de la classeArrayqui ne sont pas publiques aux classes dérivées (et

uniquement à elles). Notons qu’il n’est pas forcément nécessaire d’avoir cette partieprotected:

soit parce que la classe dérivée n’a pas besoin d’avoir accès aux méthodes non publiques de la classe de base, soit parce qu’elle y accède par des méthodes de la classe de base.

Voici le début de la classeArray(cf. page 21) modifiée :

c l a s s Array

{

protected :

const i n t n ,m; / / nombre de l i g n e s e t de colonnes . double * x ;

public :

. . . .

Notons que dans la classeArray, les champsn,metxpeuvent être vus comme des classes de base

dont dériveArray: c’est ce qui explique la syntaxe du constructeur deArray:

Array ( i n t N, i n t M) : n(N) , m(M) {

x=new double [ n*m] ; }

Si on n’utilise pas cette syntaxe, n et m ne peuvent pas être déclarésconstdansArray.

C++ connait l’héritage multiple, c’est à dire qu’une classe peut dériver de plusieurs classes de base.

Maintenant, nous voulons bien sûr utiliser les méthode de la classe de baseArray. Il y a au moins

deux cas possibles :

1. on réutilise des méthodes de la classe de base sans modification ;

2. il faut modifier certaines méthodes de la classe de base (par exemple un opérateur d’accès aux composantes). Ce deuxième cas sera expliqué plus loin.

(27)

Réutiliser des méthodes de la classe de base sans modification. — À priori, toutes les méthodes de la classe de base (ou des classes de base dans le cas d’un héritage multiple) peuvent être réutilisées, à

partir du moment où elles sontpublicouprotected. Cependant, il peut être nécessaire de lever

des ambiguïtés (méthode présente à plusieurs endroits dans une hiérarchie de classes) ou d’aider le

compilateur, en utilisant l’ordreusing. Ainsi l’en-tête deSquareMatrixcontiendra :

c l a s s SquareMatrix : public Array

{

bool f a c t o r i z e d ;

public :

using Array : : operator = ; using Array : : operator +=;

Modifier des méthodes dans une classe dérivée. — De même que nous avons créé la classe

SquareMatrix, nous pouvons créer une classeVector, dérivée deArray:

c l a s s Vector : public Array

{

public :

using Array : : operator = ; using Array : : operator +=;

/ / ! c o n s t r u c t e u r / / ! \param N t a i l l e . Vector ( i n t N) : Array (N, 1 ) { } / / ! d e s t r u c t e u r ~Vector ( ) { } / / ! r e n v o i t la t a i l l e i n l i n e i n t s i z e ( ) const { return n ; }

Comment écrire l’opérateur d’accès à une composante ? On ne peut pas utiliser directement celui deArray. On peut réécrire cet opérateur dans la classeVector:

double& operator ( ) ( i n t i )

{

# i f d e f DEBUG i f ( i < 0 | | i >=n)

throw MyException ( " Vector : i ndi c es out of bounds" ,n , i ) ; #endif

return x [ i ] ;

(28)

Une autre solution (mais qui dépend du stockage utilisé dansArrayest celle ci :

double& operator ( ) ( i n t i )

{

# i f d e f DEBUG i f ( i < 0 | | i >=n)

throw MyException ( " Vector : i ndi c es out of bounds" ,n , i ) ; #endif

return Array : : operator ( ) ( i , 0 ) ;

}

3.2. Polymorphisme

Puisque nosVectoret nosSquareMatrixsont aussi desArray, on peut écrire :

SquareMatrix A(n ) ; Vector X(n ) ; Array *p ; p=&A ; p=&X ; cout<<p−>n l i g () < < endl ;

La méthodenlig()est dans la classeArray, et donc il n’y a pas de problème particulier. En

revanche, les programmes suivant sont illicites : Vector& Xref =*p ;

Array& Xref =*p ; cout<<Xref (1) < < endl ;

UnVectorest unArray, mais unArrayn’est pas unVector!

3.2.1. Méthodes virtuelles. — C’est le cœur même du polymorphisme en C++ : comment faire

pour pouvoir appliquer la même « fonction »à des objets différents, mais qui ont tous un « ancêtre » commun ?

Voici un exemple, classique : on a des objets géométriques différents (Square,Circle,Triangle)

qui vont vivre dans le plan. On veut pouvoir appliquer les mêmes opérations (translation, rotation...) à tous ces objets. L’idée est de faire dériver tous ces objets d’une classe commune

(29)

Class Pattern { . . . . public : Pattern ( ) { . . . } ~Pattern ( ) { . . . } v i r t u a l void t r a n s l a t e ( f l o a t x , f l o a t y ) { } v i r t u a l void r o t a t e ( f l o a t teta , f l o a t xc , f l o a t yc ) { } . . . . . } ;

puis de redéfinir (éventuellement, car ce n’est peut-être pas forcément nécessaire) les méthodes dans les classes dérivées (toujours comme virtuelles) :

Class Square ; : public Pattern { f l o a t x1 , y1 , x2 , y2 ; public : Square ( f l o a t X1 , f l o a t X2 , f l o a t Y1 , f l o a t Y2 ) : Pattern ( ) { . . . } ~Square ( ) { . . . } v i r t u a l void t r a n s l a t e ( f l o a t x , f l o a t y ) { x1+=x ; y1+=y ; x2+=x ; y2+=y ; } v i r t u a l void r o t a t e ( f l o a t teta , f l o a t xc , f l o a t yc ) { . . . . . } . . . . . } ;

(30)

Pattern **P=new Patter * [ 2 ] ; / / tableau de pointeurs sur des Pattern C i r c l e C( x , y , r ) ; Square Q( x1 , x2 , y1 , y2 ) ; P[0]=&C ; P[1]=&Q; for ( i n t i =0; i <2; i ++) P [ i ] . t r a n s l a t e ( 1 . , 2 . , − 2 , 3 . ) ; for ( i n t i =0; i <2; i ++) P [ i ] . r o t a t e ( 1 . 2 , 0 , 0 , 1 . , 1 . ) ;

Le système va se débrouiller pour appeler la bonne méthode dans les boucles for.

Ce mécanisme est utilisé de manière extensive par les gestionnaires de fenêtres et de menus (Qt). Mais hélas...

 Le mécanisme de méthodes virtuelles peut mener à des performances catastrophiques.

Explication : la méthode (virtuelle) à utiliser ne peut être déterminer qu’à l’exécution, ce qui

implique un parcours de table à chaque appel.

Considérons le scénario suivant : on veut fabriquer des classes de matrices : il y aura les matrices générales, les matrices carrées, les matrices carrées symétriques, les matrices bandes, les matrices

mandes symétriques. Il est alors séduisant de créer une classe de baseMatrixdont dériveront les

autres (avec un héritage à plusieurs niveaux, probablement). Mais alors, dans chaque classe, on devra déclarer comme virtuel l’opérateur d’accès, car le stockage d’une matrice symétrique sera différent d’une matrice générale :

v i r t u a l double& operator ( i n t i , i n t j )

{ }

et bien sûr, c’est cette méthode qui sera la plus utilisée ! Performances catastrophiques garanties !

Une bonne pratique : réserver l’héritage pour les méthodes coûteuses, de sorte que le surlouât de l’appel soit négligeable, par rapport au temps passé dans la fonction.

Consulter par exemple la discussion Are virtual functions evil ? dans [Vel00].

3.2.2. Méthodes virtuelles pures et classes abstraites. — On peut ne pas créer le corps d’une

méthode dans la classe de base, qui devient une classe abstraite :

Dans la classePattern, on écrira par exemple :

v i r t u a l void t r a n s l a t e ( f l o a t x , f l o a t y ) = 0 ;

C’est le=0qui fait detranslateune méthode virtuelle pure, et de la classePatternune classe

dite abstraite.

L’intérêt est que les classes dérivées doivent obligatoirement implanter concrètement les méthodes abstraites.

(31)

On pourra méditer au genre d’application suivante :

1. on crée une classe (abstraite)RingCategory, la catégorie des anneaux, dans laquelle les

operatures + et ∗, + =, ∗ = etc. sont des méthodes virtuelles pures.

2. Les anneaux « concrets »devront alors obligatoirement implanter ces opérateurs :

c l a s s QQ: public RingCategory

{

/ / une c l a s s e de r a t i o n n e l s

public :

v i r t u a l void operator +=( const QQ& q )

{ }

. . . . } ;

Évidemmment, la question des performances se posera aussi.

Rappelons que le C++ connaît l’héritage multiple : une classe peut dériver de plusieurs classes de base ; exemple :

c l a s s A : public c l a s s B , public c l a s s C

{ . . . . } ;

(32)
(33)

CHAPITRE 4

INTERFAÇAGE (APPLICATION À LA RÉSOLUTION DE SYSTÈMES

LINÉAIRES)

On va décrire l’interfaçage avec des routines écrites en Fortran, en tout cas dans les cas simples : l’appel d’une routine Fortran (sous programme) depuis le C++.

A titre d’exemple, on va équiper la classeSquareMatrixde méthodes pour la factorisation LU et la

résolution de systèmes linéaire, avec des inconnues dans la classeVector.

Un coup d’œuil au manuel de la routinedgetrfde la bibliothèqueLapack(voir page 34) donne la

liste de ses paramètres.

Règles :

– les subroutine Fortran reçoivent leurs paramètres par adresse (cad. reçoivent en pointeur pour chaque argument).

– les noms doivent en général, du coté C(++) êtres vus en minuscules, suffixés d’un_(ceci est à

priori dépendant des compilateurs).

The leading dimension... la matrice peut être contenue dans un tableau plus grand que la taille utile : il faut fournir la taille des colonnes de ce tableau (voir l’indexation à la Fortran, page 15).

Pour appelerdgetrf, il faut écrire un fichier de prototype (appelons leprotos_lapack.hpp) :

1 # i f n d e f protos_lapack__h

2 #define protos_lapack__h 3

4 / / ! p r o t o t y p e s f o r lapack r o u t i n e s . 5 extern "C" {

6 void d g e t r f _ ( i n t *n , int *m, double* a , int * lda , int * ipiv ,

7 i n t * info ) ;

8 }

La déclarationextern "C"fait croire au compilateur C++ qu’on appelle une routine C (cela semble

nécessaire).

(34)

SUBROUTINE DGETRF( M, N, A , LDA, IPIV , INFO )

*

* −− LAPACK routine ( version 3 . 2 ) −−

* −− LAPACK i s a software package provided by Univ . of Tennessee , −− * −− Univ . of C a l i f o r n i a Berkeley , Univ . of Colorado Denver and . . .

* November 2006

*

* . . S c a l a r Arguments . .

INTEGER INFO, LDA, M, N

* . .

* . . Array Arguments . .

INTEGER IPIV ( * ) DOUBLE PRECISION A( LDA, * )

* . .

* Purpose * =======

* DGETRF computes an LU f a c t o r i z a t i o n of a general M−by−N matrix A * using p a r t i a l pivoting with row interchanges .

* The f a c t o r i z a t i o n has the form

* A = P * L * U

* where P i s a permutation matrix , L i s lower t r i a n g u l a r with unit * diagonal elements ( lower t r a p e z o i d a l i f m > n ) , and U i s upper * t r i a n g u l a r ( upper t r a p e z o i d a l i f m < n ) .

*

* This i s the r i g h t −looking Level 3 BLAS version of the algorithm . *

* Arguments * ========= *

* M ( input ) INTEGER

* The number of rows of the matrix A . M >= 0 . *

* N ( input ) INTEGER

* The number of columns of the matrix A . N >= 0 . *

* A ( input / output ) DOUBLE PRECISION array , dimension (LDA,N) * On entry , the M−by−N matrix to be factored .

* On exit , the f a c t o r s L and U from the f a c t o r i z a t i o n * A = P*L*U; the unit diagonal elements of L are not stored . *

* LDA ( input ) INTEGER

* The leading dimension of the array A . LDA >= max( 1 ,M) . *

* IPIV ( output ) INTEGER array , dimension (min(M,N) )

* The pivot i ndi ce s ; f o r 1 <= i <= min(M,N) , row i of the * matrix was interchanged with row IPIV ( i ) .

*

* INFO ( output ) INTEGER * = 0 : s u c c e s s f u l e x i t

* < 0 : i f INFO = −i , the i −th argument had an i l l e g a l value

* > 0 : i f INFO = i , U( i , i ) i s e x a c t l y zero . The f a c t o r i z a t i o n

* has been completed , but the f a c t o r U i s e x a c t l y * singular , and d i v i s i o n by zero w i l l occur i f i t i s used * to solve a system of equations .

=====================================================================

FIGURE1. Page de manuel de DGETRF.

La méthode factorize de la classeSquareMatrix:

Le listing est page 35.

– ligne 4, on alloueinfoqui sera retourné (code d’erreur éventuel),

– ligne 5, on alloue le tableauipiv, si ce n’est pas déjà fait ; NB : le destructeur de la classe SquareMatrix doit désallouer ipiv !.

– ligne 6, on recopiendans une nouvelle variablenn; pourquoi ? Rappelons-nous que n est déclaré

« const » ; passer un pointeur sur n permettrait de modifier n dans la procédure appelée, ce que le compilateur n’accepterait pas.

(35)

1 / / ! LU F a c t o r i s a t i o n . 2 i n t factorizeLU ( ) 3 { 4 i n t i n f o ; 5 i f ( i p i v ==0) i p i v =new i n t [ n ] ; 6 i n t nn=n ;

7 d g e t r f _ (&nn,&nn , x ,&nn , i p i v ,& i nf o ) ; 8

9 i f ( i n f o ==0) f a c t o r i z e d =true ;

10

11 return i n f o ;

12 }

FIGURE2. La méthodefactorizede la classeSquareMatrix

– la suite n’appelle pas de commentaires.

Résolution de systèmes linéaires

Je veux rajouter la méthodesolveà la classeSquareMatrix.

On va appeler la routinedgetrsdelapack[voir page 38).

Une version possible de la méthodesolveest donnée page 36.

Explications :

– Le paramètre est une référence à unVector, non constante, puisque le résultat est renvoyé dans

X.

– si DEBUG est positionné, on teste la compatibilité des deux objets.

– le reste de la procédure est calqué sur la méthodefactorize.

Est-ce que ce code est correct ? NON !

1. À priori, la classeArrayne sait pas ce qu’est la classeVector; il faut donc rajouter, dans

l’en-tête deArray:

#include " Vector . hpp"

Mais ce n’est pas suffisant ! 2. En effet les deux lignes

i f (n! =X . n)

et :

dgetrs_ ( "N" ,&nn,&un , x ,&nn , i p i v , X . x ,&nn,& i n fo ) ;

sont incorrectes car les champsX.netX.xsont des champs private de la classeVector.

(36)

c l a s s Vector : public Array

{

friend c l a s s SquareMatrix ; public :

. . . . .

Voici une première version de la méthodesolve:

1

2 i n t solve ( Vector& X)

3 {

4 # i f d e f DEBUG

5 i f (n! =X . n)

6 throw throw MyException ( " SparseMatrix : : solve : bad s i z e s " ,n , X . n ) ;

7 #endif

8 i f ( ! f a c t o r i z e d )

9 {

10 i n t f a c r e s =factorizeLU ( ) ;

11 i f ( f a c r e s ! = 0 )

12 throw MyException ( " SparseMatrix : : solve : factorizeLU " , f a c r e s ) ;

13 }

14 i n t nn=n ; i n t un=1 , i n f o ;

15 dgetrs_ ( "N" ,&nn,&un , x ,&nn , i p i v , X . x ,&nn,& i n fo ) ; 16 return i n f o ;

17 }

Une autre solution pour l’écriture desolve. — On peut ne pas aimer les déclarationsfriend

qui sont contradictoires avec l’encapsulage des données (mais il ne parait pas scandaleux que

matrices et vecteurs soient amis !). Voici une autre solution, sans déclarationfriend.

On munit la classeArray, dans la partieprotected, d’une méthode qui renvoiex:

protected :

i n l i n e double * data ( ) { return x ; }

puis, dans la partie publique (public) deVector, on rajoute :

using Array : : data ;

(37)

La nouvelle version desolve(notez l’utilisation des méthodessize()etdata()) :

i n t solve ( Vector& X)

{

# i f d e f DEBUG

i f (n! =X . s i z e ( ) )

throw throw MyException ( " SparseMatrix : : solve : bad s i z e s " ,n , X . n ) ; #endif

i f ( ! f a c t o r i z e d )

{

i n t f a c r e s =factorizeLU ( ) ; i f ( f a c r e s ! = 0 )

throw MyException ( " SparseMatrix : : solve : factorizeLU " , f a c r e s ) ;

}

i n t nn=n ; i n t un=1 , i n f o ;

dgetrs_ ( "N" ,&nn,&un , x ,&nn , i p i v , X . data ( ) , &nn,& i n f o ) ;

return i n f o ;

}

(38)

SUBROUTINE DGETRS( TRANS, N, NRHS, A , LDA, IPIV , B , LDB, INFO )

*

* −− LAPACK routine ( version 3 . 3 . 1 ) −−

* −− LAPACK i s a software package provided by Univ . of Tennessee , −− * −− Univ . of C a l i f o r n i a Berkeley , Univ . of Colorado Denver and NAG Ltd .. − −

* −− A p r i l 2011 −−

*

* . . S c a l a r Arguments . .

CHARACTER TRANS

INTEGER INFO, LDA, LDB, N, NRHS

* . .

* . . Array Arguments . .

INTEGER IPIV ( * )

DOUBLE PRECISION A( LDA, * ) , B( LDB, * )

* . .

* * Purpose * ======= *

* DGETRS s o l v e s a system of l i n e a r equations

* A * X = B or A**T * X = B

* with a general N−by−N matrix A using the LU f a c t o r i z a t i o n computed * by DGETRF.

*

* Arguments * ========= *

* TRANS ( input ) CHARACTER*1

* S p e c i f i e s the form of the system of equations :

* = ’N’ : A * X = B (No transpose )

* = ’T ’ : A**T* X = B ( Transpose )

* = ’C ’ : A**T* X = B ( Conjugate transpose = Transpose ) *

* N ( input ) INTEGER

* The order of the matrix A . N >= 0 . * NRHS ( input ) INTEGER

* The number of r i g h t hand sides , i . e . , the number of columns * of the matrix B . NRHS >= 0 .

* A ( input ) DOUBLE PRECISION array , dimension (LDA,N) * The f a c t o r s L and U from the f a c t o r i z a t i o n A = P*L*U

* as computed by DGETRF.

* LDA ( input ) INTEGER

* The leading dimension of the array A . LDA >= max( 1 ,N) . * IPIV ( input ) INTEGER array , dimension (N)

* The pivot i ndi ce s from DGETRF; f o r 1<= i <=N, row i of the * matrix was interchanged with row IPIV ( i ) .

* B ( input / output ) DOUBLE PRECISION array , dimension (LDB,NRHS) * On entry , the r i g h t hand side matrix B .

* On exit , the solution matrix X .

* LDB ( input ) INTEGER

* The leading dimension of the array B . LDB >= max( 1 ,N) . * INFO ( output ) INTEGER

* = 0 : s u c c e s s f u l e x i t

* < 0 : i f INFO = −i , the i −th argument had an i l l e g a l value

* =====================================================================

(39)

CHAPITRE 5

GÉNÉRICITÉ

5.1. Niveau 1 : templates

On a vu comment créer un tableau (la classeArray), qui est un tableau dedoubles. On se doute

qu’il est facile de transformer cette classe pour fabriquer des tableaux d’entier (int) par exemple,

ou de tout autre type d’objets.

Le mécanisme detemplate(1)du C++ permet cela aisément, mais va permettre aussi des

développements beaucoup plus sophistiqués. La transformation de notre classeArrayen template

de classe de tableaux est montrée page 40).

Bien sûr, ces constructions fonctionnent aussi avec l’héritage ; on peut par exemple fabriquer un template de matrices carrées qui dérivera d’un template de tableaux (mais qui pourrait aussi dériver d’une classe habituelle) :

template< c l a s s T> c l a s s SquareMatrix : public Array <T>

{ . . . . } ;

On peut aussi créer des template de fonctions :

template< c l a s s T>

T max(T array [ ] , i n t length ) { T vmax = array [ 0 ] ; for ( i n t i = 1 ; i < length ; i ++) i f ( array [ i ] > vmax) vmax = array [ i ] ; return vmax ; }

(40)

# i f n d e f Array__h #define Array__h #include <iostream > #include "MyException . hpp" using namespace std ; template< c l a s s T> c l a s s Array { const i n t n ,m; T * x ; public : typedef T ArrayData ; Array ( i n t N, i n t M) : n(N) ,m(M) { / / n=N; m=M; x=new T [ n*m] ; } ~Array ( ) { delete [ ] x ; }

Array ( const Array <T>& A ) : n(A . n ) , m(A .m) {

std : : cout<<"copy contructor "<<std : : endl ; / / n=A . n ; m=A .m;

x=new T [ n*m] ;

for ( i n t i =0; i <n*m; i ++)

x [ i ]=A . x [ i ] ; }

i n l i n e void operator =( const Array <T>& A)

{

i f (A . n! =n | | A .m! =m)

throw MyException ( " Array : : operator =( const Array <T>& A , dimensions d i f f e r " ) ; e l s e for ( i n t i =0; i <m*n ; i ++) x [ i ]=A . x [ i ] ; } i n l i n e void operator =(T v ) { for ( i n t i =0; i <m*n ; i ++) x [ i ]= v ; }

i n l i n e void operator +=( const Array <T>& A)

{

i f (A . n! =n | | A .m! =m)

throw MyException ( " Array : : operator +=( const Array <T>& A , dimensions d i f f e r " ) ; e l s e for ( i n t i =0; i <m*n ; i ++) x [ i ]+=A . x [ i ] ; } i n l i n e void operator +=(T v ) { for ( i n t i =0; i <m*n ; i ++) x [ i ]+=v ; } i n l i n e T& operator ( ) ( i n t i , i n t j ) { # i f d e f DEBUG i f ( i < 0 | | i >=n | | j < 0 | | j >=m)

throw MyException ( " Array : i nd ic es out of bounds" ,n ,m, i , j ) ; #endif

return x [ j *m+ i ] ;

}

i n l i n e i n t n l i g ( ) const { return n ; } i n l i n e i n t ncol ( ) const { return m; }

} ;

#endif

FIGURE1. Template de classe de tableaux

(41)

Voici un exemple :

1 Array <double> Ad( 1 0 , 2 0 ) ; 2 Array <int > Ai ( 1 0 , 1 0 ) ; 3 4 Ai ( 5 , 5 ) = 1 . ; 5 Ad ( 3 , 3 ) = 0 . 0 ; 6 7 i n t values [ ] = { 100 , 50 , 35 , 47 , 92 , 107 , 84 , 11 } ;

8 cout<<max( values ,8) < < endl ;

Les déclarations des lignes 1 et 2 n’appellent pas de commentaires ; remarquer à la ligne 8 que le

compilateur est capable de deviner lui-même les types des paramètres de la fonctionmax.

On a donc un mécanisme qui permet de paramétrer des types d’objets et des fonctions par d’autres types, ou par des valeurs constantes ; il est possible par exemple de fabriquer un template de tableaux de tailles fixes :

template< i n t n , i n t m> c l a s s FixedSizeDoubleArray { double x [ n*m] ; public : . . . . } ;

Notons que les compilateurs aiment les tableaux de taille fixe et plus généralement les

constructions dans lesquelles les boucles à effectuer ont une longueur connue à la compilation (ce qui lui permet par exemple de les dérouler). Avec cet exemple, on pourra écrire :

s t a t i c const i n t n=24 ,m=38;

Array <10 ,20 > A ; Array <n ,m> B ;

Car les types d’objets sont calculés à la compilation ; l’écriture suivante est illégale :

i n t n ,m;

. . . . .

n=12; m=20; Array <n ,m> A ;

5.2. Niveau 2 : une courte introduction à la Standard Template Library (STL)

On sort là de ce qui a été expliqué au paragraphe précédent qui n’est ni plus ni moins qu’un système de macro-instructions sophistiqué. Les promoteurs des templates avaient-ils en vue ce qui suit ? pas sûr ! En tout cas les compilateurs ont mis pas mal de temps avant de pouvoir compiler des constructions comme la STL.

(42)

La STL a été introduite dans C++ par O. Stepanov et A. Lee. Notons qu’ainsi, C++ est doté

naturellement d’une bibliothèque, la STL étant incorporée à la norme C++, ce qui fait différer C++ d’autres langages (comme Fortran).

La STL fournit (principalement) :

1. Des classes de conteneurs :vector, tableaux associatifsmap, listes chaînéeslistetc..

Il s’agit bien sur de conteneurs génériques, qui permettent de stocker tout type d’objet. 2. Une abstraction des pointeurs : les itérateurs. Ils permettent de parcourir les conteneurs (du

début à la fin, en sens direct ou inverse, ou de fonctionner à la manière d’opérateurs d’accès à des tableaux). Les possibilités des itérateurs dépendent des conteneurs (par exemple, une liste simplement chaînée ne permet pas une parcours rétrograde de la fin au début).

Cela permet :

– de parcourir des séquences d’objets,

– d’implanter des d’algorithmes indépendamment des structures de données pourvu que les algorithmes puisse être implantés avec les itérateurs disponibles.

3. Des algorithmes génériques : insertion/suppression, recherche et tri etc..

4. Des chaînes des caractères (string), qui pallient les faiblesses des caractères du C (possibilité

de traiter des caractères Unicode). Il n’en sera pas question ici.

Une description complete de la STL se trouve à :http://www.sgi.com/tech/stl/.

5.2.1. Un template de conteneur : set. — Voici un exemple d’ensemble (set) d’entiers (int) :

set <int > MySet ;

for ( i n t i =0; i <100; i ++)

MySet . i n s e r t ( i ) ;

for ( i n t i =50; i <150; i ++)

MySet . i n s e r t ( j ) ;

setmodélise un ensemble : chaque objet stocké est unique. Ainsi les ordresMySet.insert

ci-dessus créent l’ensemble des entiers compris antre 0 et 149.

Peut-on créer dessets de n’importe quoi ? Non, il faut que les éléments appartiennent à un

ensemble ordonné. Si les éléments ne sont pas munis d’un ordre strict(2), le compilateur détectera

l’erreur : il faut bien se souvenir que tous les template sont calculés à la compilation !

Les ensembles de nombres habituels sont ordonnés, et on peut définir son propre ordre (strict), soit pour remplacer la définition d’un ordre (strict) existant, soit pour munir un ensemble non ordonné naturellement d’un ordre (strict). Exemple :

s t r u c t l t s t r

{

bool operator ( ) ( const char * s1 , const char* s2 ) const

{

return strcmp ( s1 , s2 ) < 0 ;

} } ;

(43)

va définir une ordre sur les chaînes de caractères. On peut alors définir le type ensemble de chaînes de caractères ainsi :

set <const char * , l t s t r > A ;

La structure qui définit l’opérateur de comparaison est passée comme deuxième paramètre au

templateset.

Comment parcourir unset? En utilisant un des itérateurs associés :iterator,const_iterator,

reverse_iteratorouconst_reverse_iterator(constpour des itérateurs ne permettant pas

de modifier leset). Exemple :

for ( set <int > : : i t e r a t o r I =MySet . begin ( ) ;

I ! = MySet . end ( ) ; I ++) cout <<*I <<endl ;

Comment sont représentés lesseten machine ? Cela n’est pas précisé par la norme, mais en

pratique il s’agit d’arbres équilibrés (B-Trees) (cf. page 61) .

5.2.2. Les paires. — Le templatepair<class T,class Uest souvent très pratique. Il est aussi

intéressant parce que, si les typesTetUsont tous les deux munis d’un ordre strict alors la paire

(pair) est muni de l’ordre strict lexicographique. On peut donc déclarer par exemple :

set <pair <int , int > > A ;

mais bien sûr, là aussi on peut aussi redéfinir l’ordre surpair<int,int>.

5.2.3. Quelques autres conteneurs. —

– vector: Exemple :Vector<int>; les itérateurs ont les mêmes noms que pourset. On a aussi

un opérateur d’accès[ ]et donc un accès aléatoire.. Exemple :

vector <pair <int , f l o a t > > V ; V[ 2 ] = make_pair ( 1 , exp ( 2 . 0 ) ) ;

– map: ce sont des dictionnaires ; on en montrera plus loin une utilisation intéressante (page 45).

– hash_map. Les objets de typeTd’unemap<T>doivent être ordonnés. Si aucun ordre n’est disponible on peut utiliser des clés de hachage, ce qui sera toujours plus lent !

– stackmodélise une pile (dernier entré, premier sorti –comme dans les poubelles– Ces objets ont les méthodes push (empilement) et pop (dépilement). Exemple :

stack <int > S ; s . push ( 1 ) ; S . push ( 2 ) ;

cout<s . pop() < < ’ ’ ‘ ‘ < < s . pop() < < endl ; (on verra s’afficher : 2, puis 1).

– queue: file d’attente ; premier entré, premier sorti, avec là aussi les méthodes push et pop.. – strings.

– etc.

la liste complète des méthodes est disponible à l’url indiqué ci-dessus. Le mécanisme d’héritage (mais sans méthodes virtuelles) est intéressant à étudier, car il montre comme l’héritage permet de modéliser certains concepts.

Figure

Updating...

Références