Il est né ?
Vous êtes ici : mk0phpgtk.xgarreau.org >> aide >> devel >> cpp : Fonctions, références et modèles en C++ (g++ - partie II)
Version imprimable

Fonctions, références et modèles en C++ (g++ - partie II)

Dans ce numéro, nous passerons en revue les déclarations de fonctions en C++. Nous verrons ensuite ce que sont les références, qui ressemblent énormément à des alias, et ce à quoi on les utilise. Nous achèverons ce numéro en abordant la déclaration et l'utilisation des modèles de fonctions.



Fonctions

Si vous feuilletez ces pages, il y a de grandes chances que vous sachiez déclarer et définir une fonction en un langage, quel qu'il soit. Nous allons voir dans cet article comment cela se passe en C++. J'essaierai toutefois de ne pas perdre les débutants et détaillerai un minimum les étapes à mener à bien pour utiliser les possibilités offertes par le C++ en matière de fonctions.

Une définition de fonction en C++ ressemble à ceci :

type_de_retour nom_fonction (type_arg_1 arg_1, type_arg_2 arg2 /* , etc ...*/ ) {
    /* instructions diverses */
    return valeur_de_retour;
}

Sa déclaration ressemblera donc à :
type_de_retour nom_fonction (type_arg_1 arg_1, type_arg_2 arg2 /* , etc ...*/ );

Par exemple, créons une fonction renvoyant le double de la valeur de l'entier passé en paramètre et appliquons cette fonction.
/* ex_2_1.c++ */
#include <iostream>
using namespace std;

int double_it (int entier) {
	return 2*entier;
}

int main () {
	int var = 6;
	cout << var << " est la moitié de : " << double_it(var) << endl;
	return 0;
}

La compilation et l'exécution par g++ -o ex_2_1 ex_2_1.c++ && ./ex_2_1 vous permettra de tester ce premier exemple.

Surcharge

Admettons que l'on veuille remplacer var par un double pour utiliser var = 6.1 (à cause de l'inflation par exemple). L'appel de double_it provoquerait une erreur car un int et un double sont deux variables de types différents. On peut imaginer remplacer la fonction double_it pour qu'elle prenne en paramètre et renvoie des doubles mais dans un cas réel, il est probable que cette fonction soit appelée par ailleurs pour des valeurs de type int. Une solution à ce problème est la surcharge. La surcharge d'une fonction consiste à déclarer plusieurs fonctions faisant un traitement équivalent pour différents types de paramètres et de valeurs de retour mais ayant toutes le même nom. Notre exemple précédent deviendrait donc pour prendre en charge les valeurs de type double et int :

/* ex_2_2.c++ */
#include <iostream>
using namespace std;

int double_it(int entier) {
	cout << "Appel de double_it pour un entier." << endl;
	return 2*entier;
}

double double_it(double double_entier) {
	cout << "Appel de double_it pour un double." << endl;
	return 2*double_entier;
}

int main () {
	int int_var = 6;
	double dbl_var = 6.1;
	cout << int_var << " est la moitié de : " << double_it(int_var) << endl;
	cout << dbl_var << " est la moitié de : " << double_it(dbl_var) << endl;

	return 0;
}

Compilons et exécutons pour comprendre :
$ g++ -o ex_2_2 ex_2_2.c++ 
$ ./ex_2_2
Appel de double_it pour un entier.
6 est la moitié de : 12
Appel de double_it pour un double.
6.1 est la moitié de : 12.2

Le choix de la fonction appelée est fait en fonction du type des paramètres. Pour la valeur 6 la fonction utilisée est la première car 6 est un entier. Ensuite, pour 6.1, qui peut être représenté par un double, c'est la deuxième forme de double_it qui est utilisée.

Paramètres par défaut

Il est souvent pratique de pouvoir spécifier une valeur par défaut à un argument de fonction, ne serait ce que dans le cas où vous ajoutez des arguments au prototype d'une fonction alors que tout le monde se sert déjà d'une précédente version ne comportant pas ce dernier paramètre. Si vous en arrivez là, c'est qu'il y a un problème dans la conception de votre logiciel mais ça nous fait de bons cas d'école ;-).

Admettons donc que vous utilisiez une fonction pour afficher un caractère sur deux d'une chaîne. Votre programme ressemble plus ou moins à ça :

/* ex_2_3.c++ */
#include <iostream>
#include <string>
using namespace std;

void my_aff_car(const string s) {
	for (int i = 0 ;  i<s.length() ; i+=2) {
		cout << s[i]; 
	}
 	cout << endl;   
}

int main () {
	string s_in;
	getline (cin, s_in);
	my_aff_car(s_in);
	return 0;
}

Notez tout d'abord l'introduction de la fonction getline, qui permet de récupérer une ligne entière dans un flux (stream) passé en premier paramètre et de la placer dans une string passée en second paramètre. Ici, getline(cin, s_in) lit la ligne de texte entrée par l'utilisateur (cin) et la place dans la string s_in. Vous avez sans doute remarqué en jouant avec des constructions semblables à cin >> s que s ne contenait que le début de l'entrée de l'utilisateur si celle ci contenait des espaces... Et bien, la solution à ce problème est getline.

Après compilation, vous pouvez exécuter ce programme et constater qu'il se comporte bien et affiche, à partir du premier, un caractère sur deux de la chaîne que vous saisissez. Admettons maintenant que vous souhaitiez afficher un caractère sur trois à partir du deuxième. Vous pouvez ajouter des paramètres à la fonction. En C, cela force les anciens appels à être mis à jour, en C++ pas forcément, il suffit pour éviter cela de spécifier une valeur par défaut pour les paramètres supplémentaires.

/* ex_2_4.c++ */
#include <iostream>
#include <string>
using namespace std;

void my_aff_car(const string s, int indice_debut = 0, int step = 2) {
	for (int i = indice_debut ;  i<s.length() ; i+=step) {
		cout << s[i]; 
	}
 	cout << endl;   
}

int main () {
	string s_in;
	getline (cin, s_in);
	my_aff_car(s_in);
	my_aff_car(s_in, 1);
	my_aff_car(s_in, 1, 3);
	return 0;
}

Voici ci-dessous une exécution de ce programme :
$ ./ex_2_4
123456789ABCDEF
13579BDF
2468ACE
258BE
Vous pouvez constater que la fonction my_aff_car peut être appelée avec 1, 2 ou 3 paramètres. La limitation du principe des paramètres par défaut est que pour spécifier la valeur du troisième paramètre, vous êtes obligés de spécifier la valeur du deuxième, même si la valeur par défaut de ce dernier vous convient. Nous avions vu ce cas dans l'introduction à php-gtk du numéro de juillet-août 2002 du présent magazine, en effet, la plupart des langages récents acceptent les déclarations de paramètres par défaut dans les fonctions.
Si vous essayez d'utiliser une notation comme my_aff_car(s_in, , 3);, g++ vous récompensera avec un message ressemblant à : ex_2_4.c++:18: parse error before `,'.

Références

Comme en C, il est possible en C++ de passer les paramètres aux fonctions par valeur ou par le biais de pointeurs mais il est également possible d'utiliser la transmission par référence, comme en pascal par exemple. On utilise pour cela la notation & dans la déclaration du type des paramètres de fonctions, ce qui, vous venez de vous le dire sûrement, prête à confusion. C'est en effet la même notation que l'on utilise pour désigner l'adresse d'une variable. Le mieux pour assimiler la distinction sera de lire attentivement l'exemple qui suit.

Mais avant de taper du code, voyons à quoi peuvent bien servir les références. Il faut s'imaginer une référence comme un alias ou lien (commande ln). Un référence est un alias sur une variable, lire la référence revient à lire la variable, modifier la référence modifie la variable et la supprimer supprime la variable. Vous allez peut être vous dire que supprimer un alias n'a pas d'effet sur l'élément lié. Ce qui est faux dans le cas de l'utilisation de la commande ln sans l'option -s (cf. man ln).

On utilise les références avec les fonctions pour transmettre des variables susceptibles d'être modifiées, pour qu'elles puissent renvoyer des alias de variables pouvant être utilisées à gauche d'une assignation (lvalue en anglais, pour left value). Utiliser une référence pour passer un paramètre à une fonction évite également de devoir allouer de la mémoire dans le contexte de la fonction, puisqu'on continue de travailler sur la variable originale.

Le concept de référence est simple, son utilisation n'est pas forcément évidente, nous allons donc tout de suite voir du code (c'est bien pour ça que vous lisez ces pages, non ?). J'ai numéroté ce listing et le ferai lorsque celà s'avèrera plus pratique pour le commenter.

01 : /* ex_2_5.c++ */
02 : #include <iostream>
03 : using namespace std;
04 : 
05 : void fonction_val (int param) {
06 : 	cout << "fonction_val> ";
07 : 	cout << "param : " << &param << ":" << param << endl;
08 : 	param = 3;
09 : }
10 : 
11 : void fonction_ref (int& param) {
12 : 	cout << "fonction_ref> ";
13 : 	cout << "param : " << &param << ":" << param << endl;
14 : 	param = 4;
15 : }
16 : 
17 : void fonction_ptr (int * param) {
18 : 	cout << "fonction_ptr> ";
19 : 	cout << "param : " << &param << " : " << param << endl;
20 : 	cout << "fonction_ptr> ";
21 : 	cout << "*param : " << param << " : " << *param << endl;
22 : 	*param = 5;
23 : }
24 : 
25 : int& get_max_ref (int& param1, int& param2) {
26 : 	return ((param1>param2) ? param1 : param2);
27 : } 
28 : 
29 : int main() {
30 : 	int a = 1, b = 1;
31 : 	cout << "main    1   > ";
32 : 	cout << "a : " << &a << " : " << a << endl;
33 : 	cout << "main    1   > ";
34 : 	cout << "b : " << &b << " : " << b << endl;
35 : 	int& c = b; 
36 : 	c = 2;
37 : 	cout << "main    2   > ";
38 : 	cout << "c : " << &c << " : " << c << endl;
39 : 	cout << "main    2   > ";
40 : 	cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
41 : 	fonction_val (b);
42 : 	cout << "main    3   > ";
43 : 	cout << "a = " << a << ", b = " << b << endl;
44 : 	fonction_ref (b);
45 : 	cout << "main    4   > ";
46 : 	cout << "a = " << a << ", b = " << b << endl;
47 : 	fonction_ptr (&b);
48 : 	cout << "main    5   > ";
49 : 	cout << "a = " << a << ", b = " << b << endl;
50 : 	int& d = get_max_ref(a, b);
51 : 	d = 6;
52 : 	cout << "main    6   > ";
53 : 	cout << "a = " << a << ", b = " << b << endl;
54 : 	get_max_ref(a, b) = 7;
55 : 	cout << "main    7   > ";
56 : 	cout << "a = " << a << ", b = " << b << endl;
57 : }
A l'exécution, on obtient :
$ ./ex_2_5
main    1   > a : 0xbffff844 : 1
main    1   > b : 0xbffff840 : 1
main    2   > c : 0xbffff840 : 2
main    2   > a = 1, b = 2, c = 2
fonction_val> param : 0xbffff820:2
main    3   > a = 1, b = 2
fonction_ref> param : 0xbffff840:2
main    4   > a = 1, b = 4
fonction_ptr> param : 0xbffff820 : 0xbffff840
fonction_ptr> *param : 0xbffff840 : 4
main    5   > a = 1, b = 5
main    6   > a = 1, b = 6
main    7   > a = 1, b = 7

Quelques commentaires s'imposent ...

Au début du main, ligne 30, on déclare et définit deux variables a et b valant chacune 1 et on affiche leurs adresses en mémoire ainsi que leurs valeurs.

Ligne 35 : on déclare une référence c à la variable b. On modifie cette référence c à la ligne suivante en lui afffectant la valeur 2. On constate sur la sortie du programme que les variables b et c ont la même adresse et donc que la modification de l'une entraîne automatiquement la modification de l'autre.

Ligne 41 : on apelle la fonction fonction_val prenant un paramètre par valeur. Le paramètre transmis est ici la valeur de b. On constate donc logiquement que suite à l'exécution de la fonction, la valeur de b n'est pas modifiée. Ce qui est logique : le paramètre utilisé dans la fonction a bien la même valeur que b au début de la fonction mais une adresse différente.

Ligne 44 : L'appel de la fonction fonction_ref avec comme paramètre b ressemble à l'appel de fonction précédent. La différence réside dans la déclaration de la fonction, qui explicite que le paramètre n'est pas passé par valeur mais par référence, grâce à l'adjonction du caractère & avant le nom de la variable. On constate ici que le paramètre a la même adresse que la variable b et que, logiquement, les modifications appportées à ce dernier modifient également b.

Ligne 47 : On appelle la fonction fonction_ptr prenant un pointeur en paramètre. On voit qu'il est situé à une adresse libre (vu quelle ne sert plus au paramètre de fonction_val) et qu'il contient comme valeur l'adresse du paramètre (de b donc). On doit alors, pour modifier le paramètre, recourir à l'opérateur d'indirection *, comme on le voit aux lignes 21 et 22, lorsqu'on souhaite afficher et modifier la valeur de la variable passée en paramètre.

En guise de rappel, signalons que si l'on dispose d'une variable v, la notation &v représente son adresse et v sa valeur. Dans le cas d'une variable pointeur nommée v également, la notation &v représente l'adresse du pointeur, v l'adresse de la variable pointée et *v la valeur de la variable pointée.

Les lignes 50 à 57 présentent l'utilisation des références renvoyées par une fonction en temps que lvalue. La fonction get_max_ref renvoit une référence à la variable maximum parmi les deux variables passées en paramètres. Notez que les paramètres doivent être dans ce cas passés par référence, sans celà on renverrait une référence à une variable locale à la fonction, ce qui, comme dans le cas du renvoi d'un pointeur sur une variable locale, peut être catastrophique.
Remarquez que l'on n'est pas obligé d'affecter le retour de la fonction à une référence avant de l'utiliser mais que ceci peut être fait directement, comme démontré à la ligne 54 où on modifie directement la variable b par le biais de la référence qui est renvoyée par la fonction get_max_ref

En résumé, les références sont utiles pour modifier le paramètre d'une fonction lors de son appel, pour éviter les indirections, pour récupérer les lvalues en retour de fonctions et pour éviter de recopier des variables lors de l'exécution de fonctions. Mais les références ne sont pas que pratiques, elles se révèlent également indispensables dans certains contextes de programmation objet en c++, comme nous le verrons plus tard.

Modèles

C'est bon d'avoir un modèle dont s'inspirer dans la vie ... Mais ça n'a rien à voir avec les modèles du c++. Un modèle de fonction est en quelque sorte une élargissement du concept de surcharge présenté en début d'article. Il permet l'écriture d'une fonction (ou classe comme nous le verrons par la suite) acceptant n'importe quel type de paramètre, pour peu que celui ci soit compatible avec la notation employée dans le corps de la fonction modèle. Pour simplifier, le concept de modèle permet de passer en argument d'une fonction non seulement un paramètre mais également son type. Le but des modèles est de rendre le code toujours plus réutilisable, ce qui est une des priorités des développeurs C++.

On pourrait certainement discourir sur le concept pendant des pages entières mais je préfère vous présenter la chose via un ensemble d'exemples (on ne se refait pas ...). Tout d'abord, voyons à quoi ressemble la déclaration d'un modèle de fonction et comparons le à une déclaration de fonction habituelle. Une déclaration classique de modèle de fonction ressemble à :

template<class C> ret_type nom_func(C argument);
Ceci veut dire : "la fonction nom_func prend un argument de type C, indéterminé". Malgré ce que laisse entendre la notation class, C n'a pas besoin d'être une classe mais peut être un type intégré tel que int, double ou float. Le corps du modèle peut ensuite utiliser le type C comme un type "classique". Reprenons l'exemple ex_2_2.c++ : l'utilisation d'un modèle permet de simplifier l'écriture et de conduire à la simplification du code, comme montré ci-dessous :
/* ex_2_7.c++ */
#include <iostream>
#include <string>
using namespace std;

template<class C> C double_it (C var) {
        return var+var;
}

int main () {
        int int_var = 6;
        double dbl_var = 6.1;
        char chr_var = 'a';
	string str_var = "to";

        cout << int_var << " est la moitié de : " << double_it(int_var) << endl;
        cout << dbl_var << " est la moitié de : " << double_it(dbl_var) << endl;
        cout << chr_var << " est la moitié de : " << double_it(chr_var) << endl;
        cout << str_var << " est la moitié de : " << double_it(str_var) << endl;

        return 0;
}
En utilisant le principe de surcharge, on aurait eu besoin de 4 fonctions pour traiter les quatre différents types susceptibles d'être passés en paramètre. Grâce à l'utilisation d'un modèle, on écrit une seule fois le code de traitement, qui est décliné en autant de fonctions que nécessaire à la compilation. Lors de l'exécution, on obtient la sortie suivante :
$ ./ex_2_7
6 est la moitié de : 12
6.1 est la moitié de : 12.2
a est la moitié de : Â
to est la moitié de : toto
On constate que la fonction double_it est appelée pour chaque variable de la même façon et ce quelque soit son type. J'ai volontairement utilisé la notation return var+var plutôt que 2*var car cette dernière n'est pas compatible avec le type string. Vous devez autant que possible faire attention à ces détails lorsque vous écrivez des modèles. Lorsqu'un modèle ne peut être appliqué à un type, une erreur est générée lors de la compilation. On évite ainsi les erreurs d'exécution.

Surcharge

Notre modèle précédent fonctionne mais on pourrait préférer que double_it renvoie pour un caractère passé en paramètre une chaîne constitué de la concaténation de deux fois ce caractère plutôt que celui correspondant au double de sa valeur ascii. On peut pour celà définir une version surchargée de double_it pour le cas ou le paramètre est un caractère :

/* ex_2_8.c++ */
#include <iostream>
#include <string>
using namespace std;

template<class C> C double_it (C var) {
        return var+var;
}

string double_it (char var) {
	string s(2, var);
        return s;
}

int main () {
        int 	int_var = 6;
        double	dbl_var = 6.1;
        char	chr_var = 'a';
        string	str_var = "123";

        cout << int_var << " est la moitié de : " << double_it(int_var) << endl;
        cout << dbl_var << " est la moitié de : " << double_it(dbl_var) << endl;
        cout << chr_var << " est la moitié de : " << double_it(chr_var) << endl;
        cout << chr_var << " est la moitié de : " << double_it<char>(chr_var) << endl;
        cout << str_var << " est la moitié de : " << double_it(str_var) << endl;

        return 0;
}
L'exécution nous donne la sortie suivante :
$ ./ex_2_8
6 est la moitié de : 12
6.1 est la moitié de : 12.2
a est la moitié de : aa
a est la moitié de : Â
123 est la moitié de : 123123
On remarque que l'exécution de double_it pour un paramètre de type caractère fournit à présent un résultat différent. C'est la nouvelle fonction qui est appélée. En effet, les fonctions classiques sont utilisées prioritairement. Vient ensuite, si aucune ne convient, le choix d'un modèle de fonction.
Ce comportement peut toutefois être modifié en spécifiant le paramètre de modèle entre crochets, après le nom de la fonction. Dans ce cas, c'est le modèle approprié qui est utilisé pour le traitement. On retrouve cette notation dans l'appel double_it<char>(chr_var), qui précise que l'on veut utiliser le modèle de fonction avec un paramètre de type char.

Précision

Les modèles peuvent prendre plusieurs paramètres comme on le voit ci-dessous.

/* ex_2_9.c++ */
#include <iostream>
using namespace std;

template<class C, int i> void affiche (const C var[i]) {
	cout << i << " : ";
	for (int j = 0 ; j < i ; ++j) {
		cout << var[j] << " ";
	}
	cout << endl;
}

int main () {
        int int_var[] = { 6, 4, 3, 8 };
        double dbl_var[] = { 6.1, 4.3, 7.5 };

        affiche<int, 4>(int_var);
        affiche<double, 3>(dbl_var);

        return 0;
}
Ce modèle possède 2 paramètres, un type C et un entier i. L'entier permet de spécifier la taille du tableau passé en paramètre. C'est ici inutile puisqu'on pourrait passer i en paramètre de la fonction et ne pas spécifier la taille du tableau var mais j'utilise néanmoins cet exemple pour son côté démonstratif. On y voit bien notamment que l'on peut passer plusieurs paramètres à un modèle. Dans le cas du premier appel le modèle est "instancié" en une fonction void affiche(const int var[4]) et dans le deuxième cas en void affiche(const double var[3]). Mais en plus de cela, le paramètre i est utilisable dans le corps de la fonction, comme on le constate dans la boucle for.

Conclusion

Nous en sommes à notre deuxième article sur le C++ sans aborder la construction de classes, ni l'héritage. On pourrait continuer longtemps encore la présentation de ce langage et de ses librairies sans aborder directement les classes et les notions qui font que la programmation orientée objet rend si fiers les gens qui la pratiquent. Le harcèlement dont je suis victime ;-) m'oblige cependant à aborder la notion de classe dès le prochain article. Nous pourrons ainsi continuer notre exploration du C++ avec cette "brique de base en C++" de plus dans notre trousse à outils.

Xavier GARREAU - http://www.xgarreau.org/ - <xavier@xgarreau.org>

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.com/

Président du ROCHELUG
http://lug.larochelle.tuxfamily.org/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004

Rechercher :

Google
 
Web www.xgarreau.org