Outils d'utilisateurs

Outils du Site


parametres_arguments

Les paramètres de fonctions

Lorsque vous appelez une fonction, les variables locales dans la fonction appelée ou dans la fonction appelante ne sont pas accessibles entre les fonctions (pour rappel, une variable est accessible uniquement dans sa portée).

void foo() {
    int i { 123 };
    std::cout << j << std::endl;  // erreur, j n'est pas accessible
}
 
void bar() {
    int j { 123 };
    foo();
    std::cout << i << std::endl;  // erreur, i n'est pas accessible
}

Il est donc nécessaire d'avoir un mécanisme pour pouvoir partager des informations entre une fonction et le reste du code. Cela est réalisé via les paramètres de fonction. Pour rappel, la syntaxe générale d'une fonction est la suivante :

PARAMETRE_SORTIE NOM_FONCTION (PARAMETRES_ENTREE) {
    INSTRUCTIONS
}

Une fonction va donc pouvoir recevoir zéro, un ou plusieurs paramètres en entrée et zéro ou un paramètre en sortie. Pour commencer, vous allez voir les fonctions qui ne prennent que des paramètres en entrée, les paramètres de sortie seront vus ensuite.

Fonction avec un seul paramètre en entrée

La syntaxe pour déclarer un paramètre de fonction est assez simple : il est identique à une déclaration de variable, avec un type et un identifiant (et une valeur par défaut, qui sera vu ensuite. Considérez pour le moment que le paramètre n'a pas de valeur par défaut).

TYPE IDENTIFIANT

Par exemple, pour une fonction qui prend un entier et une autre qui prend une chaîne de caractères.

void f(int i) {
    ...
}
 
void g(std::string s) {
    ...
}

Pour la fonction f, le paramètre est int i, qui est déclaré avec un type int et un identifiant i. Même chose pour g, qui contient un paramètre de type std::string et qui s'appelle s.

En pratique, un paramètre s'utilise de la même façon qu'une variable locale (en particulier, un paramètre suit les mêmes règles concernant la portée et la durée de vie : vous pouvez utiliser un paramètre dès qu'il est déclaré et jusqu'à la fin de la fonction).

Par exemple, pour afficher les valeurs des paramètres des fonctions f et g.

void f(int i) {
    std::cout << i << std::endl;
}
 
void g(std::string s) {
    std::cout << s << std::endl;
}

Le dernier point à voir est comment appeler une fonction qui prend un paramètre. Pour cela, vous devez donner une valeur entre les parenthèses lors de l'appel de la fonction (cette valeur s'appelle un argument dans un appel de fonction). Une valeur peut être une variable (constante ou non), une littérale ou une expression.

Il existe différents modes de passage de paramètres, qui seront vus dans la suite. Chaque mode accepte ou non certaines types de valeurs. Pour le moment, le mode de passage de paramètre vu est le “passage par valeur”, ce qui autorise l'utilisation de n'importe quel type de valeur.

Par exemple, pour appeler une fonction qui prend un paramètre :

NOM_FONCTION(VALEUR)

Avec le code d'exemple précédent, vous pouvez donc écrire :

#include <iostream>
 
void f(int i) {
    std::cout << i << std::endl;
}
 
void g(std::string s) {
    std::cout << s << std::endl;
}
 
int main() {
    f(123);        // appel avec une littérale de type entier
    g("hello");    // appel avec une littérale de type chaîne
 
    const int i { 456 };
    f(i);          // appel avec une variable de type entier
    const std::string w { "world "};
    g(w);          // appel avec une variable de type chaîne
}

affiche :

123
hello
456
world 

Notez bien que même si dans cet exemple les variables dans la fonction main et la fonction f ont le même nom i, ce sont bien deux variables différentes (elles ne sont pas dans le même contexte). Comme vous le voyez avec la fonction g, il n'y a aucune obligation d'utiliser le même nom entre l'appel de la fonction et le paramètre.

Ce qui est logique et souhaitable. Comme vous pouvez appeler une fonction plusieurs fois avec des valeurs différentes (et donc des variables différentes), cela n'aurait pas de sens que l'identifiant dans le paramètre de fonction soit identique à l'identifiant utilisé pour appeler la fonction.

const int i { 123 };
f(i);  // ok
 
const int j {456 };
f(j);  // ok

Paramètre et argument

Il existe deux termes qui sont proches, mais qui ont un sens légèrement différent : “paramètre” et “argument”. Dans la déclaration de la fonction, vous trouvez des “paramètres de fonction”. Dans l'appel de fonction, ce sont des “arguments de fonction”.

void f(paramètres...) {
}
 
 
int main() {
    f(arguments...);
}

Beaucoup de personnes confondent les deux notions et cela n'est généralement pas problématique. Mais c'est souvent à ce genre de petits détails que l'on reconnait un développeur qui connait son sujet.

Fonction avec plusieurs paramètres en entrée

Pour déclarer une fonction qui prend plusieurs paramètres, vous devez donner la liste des paramètres séparés par une virgule.

TYPE IDENTIFIANT, TYPE IDENTIFIANT, TYPE IDENTIFIANT...

Il n'y a pas de limite théorique dans la norme C++ sur le nombre de paramètres que vous pouvez écrire (A vérifier). Par contre, les compilateurs imposent en général une limite. Et de toute façon, une fonction qui prend trop de paramètres deviendra difficilement lisible et c'est généralement le signe qu'il y a un problème d'organisation dans le code.

Par exemple, pour écrire une fonction qui prend en paramètre un entier et une chaîne :

void f(int i, std::string s) {
    std::cout << i << std::endl;
    std::cout << s << std::endl;
}

Une fonction qui prend plusieurs paramètres s'appelle en donnant la liste des arguments, séparés aussi par des virgules. Chaque argument est indépendant, vous pouvez mélanger des variables, des constantes, des littérales, des expressions, selon ce qui est autorisé pour chaque paramètre (selon le mode).

f(123, "hello");

std::pair, std::tuple, structures de données

Même si un paramètre correspond à un type, rien n'interdit que ce soit un type complexe, comme par exemple un std::pair, un std::tuple, ou une structure de données. Chacun de ces types peut contenir plusieurs autres types, ce qui permet en pratique de passer en paramètres de fonction autant de types que vous souhaitez.

Il faut donc éviter de passer trop d'information à une fonction et faire en sorte que les paramètres aient une cohérence entre eux. Par exemple, std::string est une structure de données complexe, qui contient d'autres informations en interne. Mais cela ne pose pas de problème, puisque ce type est manipulé comme un tout cohérent, sans avoir besoin de connaître ce qu'il contient exactement. Seul ce que représente ce type (une chaîne de caractères) est important pour comprendre le sens de la fonction.

Avec std::pair et std::tuple, la situation est un peu différente des structures de données. En utilisant ces types, vous pouvez passer n'importe quelles informations, même des informations qui n'ont pas de lien logique entre elles. Même si cela semble être une plus grande liberté, cela nuit en fait à la compréhension du code. Il est donc assez rare de trouver des fonctions utilisant std::pair et std::tuple en paramètres. À la place, il est préférable de créer une structure de données, qui aura un nom explicite et facilitera la compréhension du code.

Correspondance entre les paramètres et arguments

La liste des arguments doit correspondre à la liste des paramètres, aussi bien le nombre que les types.

main.cpp
#include <iostream>
 
void f() {
    std::cout << "f()" << std::endl;
}
 
void g(int i) {
    std::cout << "g() avec i=" << i << std::endl;
}
 
int main() {
    f();    // ok
    f(123); // erreur, trop d'argument
 
    g();    // erreur, pas assez d'argument
    g(123); // ok
}

Messages d'erreur des compilateurs

Lorsqu'un compilateur trouve une fonction qui pourrait correspondre à un appel de fonction (avec le même nom de fonction), mais dont le nombre d'arguments ou les types ne correspondent pas, il produit généralement un message d'erreur indiquant qu'il ne trouve pas de fonction correspondante (“no matching function”) et donne la liste des fonctions qui pourraient convenir.

Il est donc très important de bien lire les messages du compilateur, cela facilite grandement la résolution des problèmes, en indiquant où chercher.

Par exemple, un message d'erreur classique produit par le compilateur Clang dans cette situation :

main.cpp:13:5: error: no matching function for call to 'f'
    f(123);
    ^
main.cpp:3:6: note: candidate function not viable: 
requires 0 arguments, but 1 was provided
void f() {
     ^

Le compilateur indique qu'il ne trouve pas de fonction qui s'appelle f et dont les paramètres sont compatibles avec l'appel f(123) (“no matching function”). Il indique à la ligne suivante qu'il connait une fonction f qui pourrait être candidate (“candidate function”), mais qui prend zéro argument (“requires 0 arguments”), alors que l'appel utilise un argument (“but 1 was provided”).

Pour l'appel de la fonction g, le message est le suivant :

main.cpp:15:5: error: no matching function for call to 'g'
    g();   
    ^
main.cpp:7:6: note: candidate function not viable: requires single 
argument 'i', but no arguments were provided
void g(int i) {
     ^

De la même manière, le compilateur indique qu'il ne trouve pas de fonction correspondant à l'appel g(), mais qu'il trouve une fonction qui se nomme g et qui prend un argument.

Correspondance entre les types

En plus d'avoir le nombre d'arguments dans un appel de fonction qui doit correspondre au nombre de paramètres déclarés dans une fonction, il faut aussi que les types soient exactement les mêmes ou être implicitement convertibles.

Un type est “implicitement convertible” (sous entendu “par le compilateur”) lorsque le compilateur connaît une conversion entre les types et qu'il a le droit de réaliser automatiquement cette conversion.

Par exemple, le compilateur saura convertir sans problème un argument de type entier int en paramètre de type réel double ou une littérale chaîne (de type const char*) en paramètre de type std::string. Par contre, il n'est pas possible de convertir un std::string en std::vector<double> (conversion impossible) ou en const char* (cette conversion est possible en appelant la fonction membre std::string::c_str, mais le compilateur ne peut pas l'appeler implicitement : il faut que la conversion soit écrite explicitement par le développeur).

void f(double d) {
}
 
void g(std::string s) {
}
 
int main() {
    f(123);     // conversion de int en double
    g("hello"); // conversion de const char* en string
}

En revanche, la conversion (par exemple) d'un argument réel en paramètre entier produira une erreur d'arrondi (comme vous avez vu lors de l'initialisation d'une variable, avec le narrowing).

main.cpp
#include <iostream>
 
void f(int i) {
    std::cout << i << std::endl;
}
 
int main() {
    f(12.34);
    const double x = 12.34;
    f(x);
}

affiche :

main.cpp:8:7: warning: implicit conversion from 'double' to 
'int' changes value from 12.34 to 12 [-Wliteral-conversion]
    f(12.34);
    ~ ^~~~~
main.cpp:10:7: warning: implicit conversion turns floating-point 
number into integer: 'const double' to 'int' [-Wfloat-conversion]
    f(x);
    ~ ^
2 warnings generated.
12
12

Notez que le compilateur fait la différence entre la conversion d'une littérale (-Wliteral-conversion) et la conversion d'une valeur réelle (-Wfloat-conversion). Le résultat de ces arrondis est visible dans le résultat affiché, puisque seules les parties entières sont conservées.

Pour terminer, lorsque les types des arguments et des paramètres ne sont pas du tout compatibles, le compilateur indique une erreur spécifique (no known conversion).

main.cpp
#include <iostream>
 
void f(int i) {
    std::cout << i << std::endl;
}
 
int main() {
    f("hello");
}

affiche :

main.cpp:8:5: error: no matching function for call to 'f'
    f("hello");
    ^
main.cpp:3:6: note: candidate function not viable: no known 
conversion from 'const char [6]' to 'int' for 1st argument
void f(int i) {
     ^
1 error generated.

Retour de fonction

Le paramètre de retour de fonction est une valeur qui est transmise uniquement depuis la fonction vers le code appelant. Pour rappel, la signature générale d'une fonction qui retourne une valeur est la suivante :

PARAMETRE_RETOUR NOM(...)

Lorsqu'une fonction ne retourne aucune valeur, le PARAMETRE_RETOUR est remplacé par void (qui est un type très particulier, puisqu'il représente “pas de type”). Si la fonction retourne une valeur, PARAMETRE_RETOUR est remplacé par le type de la valeur que la fonction retourne.

int                 f();  // retourne un entier
std::string         g();  // retourne une chaîne
std::vector<double> h();  // retourne un tableau

Dans le corps de la fonction, la valeur retournée est indiquée par le mot-clé return suivi de la valeur à retourner. Comme pour les paramètres d'entrées de fonctions, la valeur peut être une littérale, une variable, une constante ou une expression. Et le type de la valeur doit être identique ou implicitement convertible dans le type de retour de la fonction.

int f() {
    return 1;  // retourne la valeur entière 1 (littérale)
}
 
int g() {
    const int i { 123 };
    return i;  // retourne la valeur entière 123 (constante)
}

Bien sûr, rien n'interdit d'utiliser des paramètres d'entrée pour calculer la valeur à retourner. Par exemple :

int f(int i) {
    return ++i;  // retourne la valeur entière i+1 (expression)
}

Cette dernière syntaxe montre bien l'origine des termes “paramètres d'entrée” et “paramètre de sortie” : une fonction prend des valeurs, réalise des calculs dessus, puis retourne une valeur comme résultat. C'est très proche du concept de fonction en mathématique.

Fonction pure

Une fonction similaire au dernier code, qui prend uniquement des paramètres en entrée sans les modifier, puis retourne une valeur, est appelée une fonction pure en programmation fonctionnelle. Cela aura un intérêt particulier, notamment pour comprendre le comportement d'une fonction et écrire des tests sur cette fonction. Cela sera vu dans le chapitre sur les tests unitaires.

Mais dans de nombreux cas, une fonction ne sera pas pure. Une fonction deviendra impure si elle :

  • modifie les paramètres en entrée (ce qui est possible en utilisant des indirections, qui seront vues dans le chapitre suivant) ;
  • en accédant à des variables externes à la fonction (des variables globales).

La simple utilisation de std::cout est suffisante pour rendre une fonction impure.

La valeur retournée par une fonction est directement accessible dans le code appelant la fonction. L'appel de la fonction peut être directement utilisé comme valeur, ce qui permet de l'affecter à une variable ou de l'utiliser dans une expression. (En fait, un appel de fonction EST une expression).

main.cpp
#include <iostream>
 
int f() { return 123; }
int g(int i) { return ++i; }
 
int main() {
    // initialisation dans une variable
    const auto i = f();
 
    // utilisation dans une expression
    std::cout << (f() + g(123)) << std::endl;
}

Une fonction qui retourne une valeur doit obligatoirement avoir au moins un return. Si ce n'est pas le cas, le compilateur produit un message d'erreur :

main.cpp:3:15: warning: control reaches end of non-void function [-Wreturn-type]
 }
 ^

(Ce qui signifie “atteint la fin de la fonction qui n'est pas void”).

Il est possible d'avoir plusieurs return dans une fonction. Lorsque la fonction arrive à un return, elle se termine immédiatement et la suite du code appelant est exécuté.

int f() {
    return 123;
    const int i { 456 };
    return i;
}

Le code à la suite du premier return n'est jamais exécuté et la fonction retourne toujours 123. (Cela n'est pas très utile dans un code aussi simple d'avoir plusieurs return, cela sera intéressant quand vous concevrez des algorithmes plus complexes).

Le mot-clé return peut également être utilisé avec une fonction qui ne retourne aucune valeur. Dans ce cas, return n'est suivi d'aucune valeur.

void f(int i) {
    return;
    ++i;
}

La fonction main

Un cas particulier avec la fonction main : vous avez peut être réalisé que la signature de cette fonction indique qu'elle doit retourner une valeur entière int. Mais dans les codes d'exemple de cours, le mot-clé return n'a jamais été utilisé jusqu'à présent.

La raison est que la fonction main est la seule exception à cette obligation de toujours avoir au moins un return dans une fonction qui retourne une valeur. Dans le cas de la fonction main, si return n'est pas présent, la fonction main retourne par défaut la valeur 0 (qui indique que le programme s'est déroulé correctement).

Valeurs par défaut

Vous avez vu que pour appeler une fonction, il est nécessaire de fournir autant d'arguments lors de l'appel qu'il y a de paramètres dans la déclaration de la fonction. En fait, ce n'est pas tout à fait vrai. Il est possible de fournir des paramètres par défaut lors de la déclaration d'une fonction. Lors de l'appel, ces paramètres seront optionnels et il sera possible de ne pas fournir d'arguments pour ceux-ci.

Pour donner un paramètre par défaut à une fonction, la syntaxe est assez proche d'une initialisation de variable. La différence est que seule la syntaxe avec le signe = est acceptée.

TYPE PARAMETRE = VALEUR_PAR_DEFAUT

Par exemple, pour écrire une fonction qui peut prendre un paramètre entier optionnel.

main.cpp
#include <iostream>
 
void f(int i = 0) {
    std::cout << i << std::endl;
}
 
int main() {
    f(123);  // appel avec un argument
    f();     // appel avec le paramètre par défaut
}

affiche :

123
0

Lorsqu'une fonction a plusieurs paramètres, une nouvelle règle s'ajoute : il n'est pas possible de faire suivre un paramètre avec une valeur par défaut par un paramètre sans valeur par défaut.

void f(int i = 0, int j = 0, int k = 0);  // ok
void g(int i    , int j = 0, int k = 0);  // ok
void h(int i = 0, int j = 0, int k    );  // erreur

La fonction h possède un paramètre k sans valeur par défaut, alors que les paramètres i et j ont des valeurs par défaut.

La fonction f a trois paramètres optionnels, alors que la fonction g a un paramètre obligatoire (i) et deux paramètres optionnels (j et k).

f(1, 2, 3);   // i=1, j=2, k=3
f(1, 2);      // i=1, j=2, k=0
f(1);         // i=1, j=0, k=0
f();          // i=0, j=0, k=0
 
g(1, 2, 3);   // i=1, j=2, k=3
g(1, 2);      // i=1, j=2, k=0
g(1);         // i=1, j=0, k=0
g();          // erreur, i n'est pas optionnel
parametres_arguments.txt · Dernière modification: 2018/06/23 20:25 par winjerome