Ceci est une ancienne révision du document !
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|
Jusque maintenant, vous avez écrit du code impératif : les instructions sont exécutées une par une, dans l'ordre où elles apparaissent dans le code.
#include <iostream> int main() { std::cout << "A" << std::endl; std::cout << "B" << std::endl; std::cout << "C" << std::endl; std::cout << "D" << std::endl; }
affiche :
A B C D
En programmation procédurale, une suite d'instructions est placée dans une procédure (on parle de “fonction” en C++) pour pouvoir être appelé plusieurs fois.
Le code suivant créé une fonction f
qui contient 2 instructions et est appelée 2 fois.
#include <iostream> void f() { std::cout << " 1" << std::endl; std::cout << " 2" << std::endl; } int main() { std::cout << "A" << std::endl; f(); std::cout << "B" << std::endl; f(); std::cout << "C" << std::endl; }
affiche :
A 1 2 B 1 2 C
Pour suivre le déroulement d'un tel programme :
«
et de std::endl
. De fait, la distinction entre programmation impérative et procédurale est purement pédagogique, un code sera généralement procédurale, même si vous n'écrivez pas explicitement des fonctions.
Ce type de programmation est encore relativement simple à suivre, il suffit de lire les instructions une par une. La difficulté vient si vous avez beaucoup de fonctions, il est pénible d'arrêter la lecture toutes les 2 lignes pour aller lire une autre partie de code. (Essayez de lire un livre et d'aller consulter un dictionnaire tous les 2 phrases, vous comprendrez).
En pratique, cela ne sera pas nécessaire si vous écrivez correctement vos fonctions, en leur donnant un nom explicite. Avec le nom de la fonction, vous pourrez comprendre ce qu'elle fait et continuer la lecture du code, sans devoir aller lire les instructions dans la fonction appellée.
Et en fait, c'est ce que vous faite depuis le début de ce cours. Vous avez déjà utiliser des fonctions, comme par exemple les algorithmes de la bibliothèque standard. Par exemple, le code suivant :
vector<int> v { 1, 8, 3, 5, 2, 9 }; std::sort(begin(v), end(v)); std::cout << v.front() << std::endl;
Lorsque vous lisez la ligne avec std::sort
, vous comprenez que le tableau est trié et que vous afficher ensuite le premier élément. Vous n'avez pas besoin d'aller lire le code de cette fonction pour comprendre ce code et déterminé que ce code va afficher la valeur “1” (première valeur du tableau après le tri).
C'est une règle générale qui faut retenir quand vous créez vos fonctions : il faut pouvoir les comprendre et les utiliser sans devoir aller lire le code de la fonction.
Un autre exemple de fonction que vous connaissez bien maintenant : la fonction main
:
int main() { }
Une fonction est définie par :
PARAMETRE_SORTIE NOM_FONCTION (PARAMETRES_ENTREE) { INSTRUCTIONS }
Les paramètres en entrées et sortie seront détaillés ensuite. Si pas d'information à passer, il est possible de ne pas avoir de paramètre en entrée et d'utiliser void
comme paramètre de sortie (qui signifie “pas d'information” dans ce cas).
void NOM_FONCTION () { INSTRUCTIONS }
Le nom de la fonction suit les mêmes règles que les noms des variables :
_
_
: ceci n'est pas une interdiction du langage, un nom peut commencer par _
, mais c'est généralement réservé à la STL. Donc a éviter en général)Par exemple :
void f() {} void g() {} void h() {} void foo() {} void bar() {} void une_fonction_quelconque() {} void UneAutreFonction() {} void uNeFoNcTiOnPaSlIsIbLe() {} void f1() {} void f2() {}
Note : dans ces codes, {}
indique un bloc d'instructions… sans instructions. Donc ces fonctions ne font rien, ce sont juste des exemples pour presenter la syntaxe.
Ces noms de fonction sont valides. Les 2 premières “f” et “g”, etc ne sont pas explicite, donc impossible de savoir ce qu'elles font sans lire leur code. Donc mauvais nom en général (mais utilisé souvent comme des codes d'explication, pour nommer des fonctions qui n'ont pas d'autre utilité que d'expliquer quelque chose ou pour lequel le nom importe peu. Ne pas utiliser dans des codes réels).
Les 2 fonctions suivantes “une_fonction_quelconque” et “UneAutreFonction” ont des noms plus explicite. Notez l'utilisation des _
et majuscule pour faciliter la lecture. Forme a préférer dans vos codes, avec des noms explicites.
La suivante est un exemple de fonction valide, mais peu lisible à cause de la mauvaise utilisation des majuscules.
Les 2 dernières utilise un nom avec un numéro. C'est également un exemple de nom peu explicite, à éviter dans un code réel.
Exemple de noms pas valides :
void 123f() {} // commence par un chiffre void une&fonction() {} // caractère illégal
Pour appeler une fonction qui ne prend pas d'arguments et qui ne retourne pas d'information, vous pouvez l'appeler directement en utilisant son nom et des parentheses vides. Vous avez utilise cette syntaxe de nombreuses fois, par exemple pour la fonction size
de std::string
ou la fonction clear
de std::vector
.
nom_fonction();
Bien sur, vous pouvez appeler plusieurs fois une meme fonction (comme vu dans le second code) ou appeler une fonction depuis une autre fonction. (C'est d'ailleurs ce que vous faites quand vous appeler une fonction quelconque depuis la fonction main.
void g() {} void f() { g(); } // f appelle g int main() { f(); f(); // plusieurs appels de la fonction f }
Une fonction qui est definie dans une autre fonction est appellee une fonction imbriquee (nested function). En C++, a part le cas particulier des fonctions lambdas que vous verrez ensuite, une fonction ne peut pas etre imbriquee dans une autre fonction.
void f() { void g(); // declaration d'une fonction imbriquee : erreur h(); // appel d'une fonction : ok }
Notez bien la difference entre declarer une fonction et appeler une fonction : une declaration contient toujours dans l'ordre un type, un identifiant et des parentheses.
Les fonctions doivent etre declarees dans un espace de noms - qui sera vu dans le chapitre sur la creation de bibliotheques - ou dans une classe - qui sera vu dans la partie programmation orientee objet. Quand vous declarer une fonction directement en dehors de la fonction main
, comme vous le faites depuis le debut de ce chapitre, vous les declarez en fait dans un espace de noms global et anonyme. Cela sera egalement vu bientot.
Un cas particulier de fonction qui appelle une fonction : il est possible d'ecrire une fonction qui s'appelle elle meme. Le code suivant est valide (tout au moins en termes de syntaxe) :
void f() { f(); // f appelle f }
Un code qui s'appelle lui-meme est appele un code recursif. C'est une approche interessante pour resoudre de tres nombreuses problematiques. Vous verrez dans les exercices et complements quelques exemples d'algorithmes recursifs.
Cependant, le code precedent est trop simpliste et produira un crash a l'execution du programme. La raison est tres simple : la fonction f va appeler la fonction f qui va appeler la fonction f qui va appeler la fonction f… et ainsi de suite, a l'infini. Ou plus precisement, puisqu'un ordinateur n'a pas des capacites infinies, jusqu'a ce que les ressources allouees au programme soient toutes utilisees et que le programme crash.
Un petit rappel sur les notions de portee et de duree de vie. Pour le moment, vous n'avez utilise que des variables locales a une fonction. Une telle fonction est declaree dans un bloc dans une fonction (dans la fonction main
jusque maintenant, mais c'est valide pour n'importe quelle fonction que vous pouvez creer). Cette variable est utilisable a partir du moment ou elle est definie et jusqu'a la fin du bloc.
{ int i {}; // definition d'une variable i ... } // fin de portee de la variable i
Les notions de portee (scope) et duree de vie (lifetime) sont tres proches (pour le moment) :
Pour des variables locales, la portee et la duree de vie d'une variable sont les memes, mais vous verrez qu'il existe une autre categorie de variables (les variables dynamiques), dont la portee et la duree de vie peuvent etre differents.
Il existe en fait une troisieme categorie : les variables globales, qui ont generalement une portee globale (accessible n'importe ou dans le programme - c'est par exemple le cas de std::cout
) et une duree de vie permanente (creation au lancement du programme et destruction lorsque le programme se termine).
L'utilisation des variables globales est problematique en termes de conception, elles doivent etre evitee au maximum. C'est a dire toujours, sauf quand vous avez de tres bonnes justifications pour ne pas respecter cette regle.
Pour terminer ce rappel, les variables sont accessible dans le meme bloc ou elles sont definies et dans les blocs enfants, mais pas dans les blocs parents ou les blocs qui n'ont pas de relation hierarchique.
int main() { { int i {}; { std::cout << i << std::endl; // ok, bloc enfant } std::cout << i << std::endl; // ok, meme bloc } std::cout << i << std::endl; // erreur, bloc parent { std::cout << i << std::endl; // erreur, bloc independent } }
Cette notion est tres importante a comprendre lorsque vous appelez des fonctions : les variables locales dans ces fonctions sont detruites a la fin de la fonction. Si vous appelez plusieurs fois une fonction, les variables locales sont creees puis detruites a chaque appel. Il n'est pas possible de transmettre des informations entre les differents appels via des variables locales.
Considerez par exemple le code suivant.
#include <iostream> void f() { int i; // volontairement non initialisee std::cout << i << std::endl; i = 123; std::cout << i << std::endl; } int main() { f(); f(); }
Si vous dérouler les appels de fonction, vous pourriez penser que le code sera le suivant (en copiant-collant le code de la fonction f
a la place des appels de fonction) :
#include <iostream> int main() { int i; // volontairement non initialisee std::cout << i << std::endl; i = 123; std::cout << i << std::endl; std::cout << i << std::endl; i = 123; std::cout << i << std::endl; }
Et donc vous pouvez vous attendre a ce que le programme affiche :
xxx // une valeur aleatoire quelconque, puisque i n'est pas initialise 123 123 123
Mais il faut bien comprendre que ce qui se passe en realite est que chaque appel de fonction va creer une nouvelle variable locale puis la detruire. Donc meme si le nom est le meme, c'est comme si la variable etait differente. Un code plus correct serait celui ci :
#include <iostream> int main() { int i_1; // volontairement non initialisee std::cout << i_1 << std::endl; i_1 = 123; std::cout << i_1 << std::endl; int i_2; // volontairement non initialisee std::cout << i_2 << std::endl; i_2 = 123; std::cout << i_2 << std::endl; }
Avec ce code, vous comprenez bien qu'il n'y a pas de raison que les deux appels a la fonction f possedent la meme valeur dans i
. Faites bien attention a cela.
0 123 123 123
La raison est que le compilateur optimise au mieux le programme génère a partir de votre code et va donc probablement utiliser le meme emplacement en memoire entre les deux appels de fonction, ce qui donnera l'impression que la valeur de i
est transmise.
Mais bien sur, sur un code plus complexe, le comportement pourra etre différent. La norme C++ ne garantie pas que le comportement du programme sera de toujours afficher une valeur aléatoire si la variable n'est pas initialisée ou d'afficher une valeur différente entre deux appels de fonction, mais simplement que aucun comportement n'est garantie. Et sans cette garantie, vous ne pouvez pas être sur de ce que va faire votre programme.
Les variables locales a une fonction ne sont donc pas partageable avec le code qui appelle une fonction, avec d'autres fonctions ou entre plusieurs appels d'une même fonction. Il est donc nécessaire de 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ées 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 vu ensuite.
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 une 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 tout simplement donner une valeur entre les parenthèses lors de l'appel de la fonction.
Une valeur pourra être une variable constante ou non et/ou une littérale, selon les types de paramètres. Pour le moment, les paramètres sont “passés par valeur”, ce qui autorise l'utilisation de littérales et de variables. Les différents modes de passage de paramètres seront vu dans la suite de ce chapitre.
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 entière g("hello"); // appel avec une littérale chaîne const int i { 456 }; f(i); // appel avec une variable const std::string w { "world "}; g(w); // appel avec une variable }
affiche :
123 hello 456 world
Notez bien que même si dans cet exemple les variables dans la fonction main
et la fonctions 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. (Heureusement ! Comme vous pouvez appeler une fonction plusieurs fois avec des valeurs différentes, cela serait très limitant de devoir utiliser le même nom).
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 trouver 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 personne 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.
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 prendre 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.
f(123, "hello");
Même si un paramètre correspond à un type, rien n'interdit que ce soit un type complexe, comme par exemple une 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 information 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.
Avec std::pair
et std::tuple
, la situation est différente. 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. A 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.
La liste des arguments doit correspondre à la liste des paramètres :
#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 }
Exemple de message d'erreur pour f
(clang)
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 compatible 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.
En plus d'avoir le nombre d'arguments dans un appel de fonction qui correspond au nombre de paramètres déclarés dans une fonction, il faut aussi que les types soient les mêmes ou être implicitement convertible par le compilateur. 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
.
void f(double d) { } void g(std::string s) { } int main() { f(123); // conversion de int entre 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).
#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 56
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 compatible, le compilateur indique une erreur spécifique (no known conversion).
#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.
On peut souhaiter pouvoir appeler une fonction avec et sans un argument. Par exemple f qui prend un entier ou 0 si on en donne aucune valeur
Première solution, surcharger la fonction :
void f() { std::cout << "f()" << std::endl; } void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } int main() { f(); // ok, appel de f() f(123); // ok, appel de f(int i) }
Le compilateur trouve à chaque fois deux fonctions avec le même nom, mais pas d’ambiguïté pour savoir laquelle appeler.
Possibilité de simplifier en donnant une valeur par défaut à un paramètre :
void f(int i = 0) { std::cout << "f(int) avec i=" << i << std::endl; }
Dans ce cas, on indique que f peut prendre un entier. Si on ne donne pas de valeur, le compilateur peut utiliser la valeur par défaut :
int main() { f(); // ok, appel de f(int i) avec i = 0 f(123); // ok, appel de f(int i) avec i = 123 }
Bien sûr, il ne faut pas laisser les 2 fonctions, pour éviter les ambiguité :
void f() { std::cout << "f()" << std::endl; } void f(int i = 0) { std::cout << "f(int) avec i=" << i << std::endl; } int main() { f(); // erreur, appel de f() ou de f(int i) avec i = 0 ? }
Idem dans l'autre sens :
void f() { int i {}; } int main() { f(); // i existe dans f() std::cout << i << std::endl; // erreur, i n'existe pas dans ce bloc }
Une variable déclarée localement dans une fonction ne sera pas accessible dans le code qui appelle cette fonction. Utilisation de retour de fonction, permet de retourner 1 seule valeur. Utilisation du mot-clé return pour indiquer la valeur que la fonction doit retourner et remplacer void par le type de la valeur que l'on veut retourner.
int f() { int const i { 123 }; return i; } int main() { int const j = f(); std::cout << j << std::endl; }
Lorsque l'on appelle f, la variable i dans f est créée et initialisée avec la littérale 123. après le return, la valeur de i est retournée au code appelant, la variable j est créée et initialisée en copiant la valeur retournée par la fonction f (elle copie i) tandis que la variable i est détruite.
retourner directement une valeur :
int f() { return 123; } int main() { int const j = f(); std::cout << j << std::endl; }
Portée de variable fait que l'on peut utiliser 2 variables de même noms si portée différentes. Par exemple :
int f() { int const i { 123 }; return i; } int main() { int const i = f(); std::cout << i << std::endl; }
Il faut bien comprendre ici que même si les 2 variables dans la fonction f et dans main s'appellent toutes les 2 “i”, ce sont 2 variables différentes.
Mot clé return retourne immédiatement de la fonction. Si on écrit :
int f() { int const i { 123 }; return i; std::cout << "on est après le return" << std::endl; // n'est jamais exécuté } int main() { int const i = f(); std::cout << i << std::endl; }
le cout après le return n'est pas exécuté.
(ou overloading ou polymorphisme ad-hoc)
Polymorphisme : plusieurs fonctions de même nom. Des fonctions peuvent avoir le même nom, tant que les paramètres sont différents :
void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(string s) { std::cout << "f(string) avec s=" << s << std::endl; }
Le compilateur choisit la fonction correspondante, selon le type que l'on donne en argument :
void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1); // 1 est une littérale de type int f(2L); // 2L est une littérale de type long int int i { 1 }; f(i); long int l { 2 }; f(l); }
affiche :
f(int) avec i=1 f(long int) avec i=2 f(int) avec i=1 f(long int) avec i=2
Le compilateur commence par rechercher s'il connait une fonction avec le nom correspondant. Par exemple pour f(1), il trouve 2 fonctions : f(int) et f(long int). Ensuite il regarde si l'un des types en paramètre correspondant au type en argument. Ici, c'est le cas, il appelle donc f(int).
Si on écrit :
#include <iostream> void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1); // 1 est une littérale de type int }
Le compilateur trouve la fonction f, mais le paramètre ne correspond pas. Il regarde s'il peut faire une conversion. Ici, oui, on peut convertir implicitement un int en long int. il convertie donc 1 en 1L et appelle f(long int).
S'il ne trouve pas de conversion possible, il lance un message d'erreur. Par exemple, si on appelle f(“du texte”), le compilateur donne :
main.cpp:19:5: error: no matching function for call to 'f' f("une chaine"); ^ main.cpp:3:6: note: candidate function not viable: no known conversion from 'const char [11]' to 'int' for 1st argument void f(int i) { ^ main.cpp:7:6: note: candidate function not viable: no known conversion from 'const char [11]' to 'long' for 1st argument void f(long int i) { ^ 1 error generated.
Ce qui signifie qu'il ne trouve aucune fonction correspond à l'appel de f(“une chaine”), mais qu'il a 2 candidat (2 fonction qui ont le même nom) mais sans conversion possible (“no known conversion”).
Au contraire, dans certain cas, il aura plusieurs possible possible, soit parce que vous déclarez par erreur 2 fonctions avec les mêmes paramètres, soit parce que le compilateur peut faire 2 conversions pour 2 types. Dans le premier cas :
void f() { std::cout << "première fonction f" << std::endl; } void f() { std::cout << "seconde fonction f" << std::endl; }
produit le message :
main.cpp:7:6: error: redefinition of 'f' void f(int i) { ^ main.cpp:3:6: note: previous definition is here void f(int i) { ^
Quand le compilateur arrive à la ligne 7 et rencontre la seconde fonction f (qu'il connait déjà), il prévient qu'il connait déjà (“redefinition of 'f'”) et que la première version (“previous definition is here”) se trouve à la ligne 3.
L'autre cas est si plusieurs fonctions peuvent correspondent, l'appel est ambigu. Par exemple :
#include <iostream> void f(int i) { std::cout << "f(int) avec i=" << i << std::endl; } void f(long int i) { std::cout << "f(long int) avec i=" << i << std::endl; } int main() { f(1u); // 1 est une littérale de type unsigned int }
affiche le message d'erreur :
main.cpp:12:5: error: call to 'f' is ambiguous f(1u); // 1 est une littérale de type int ^ main.cpp:3:6: note: candidate function void f(int i) { ^ main.cpp:7:6: note: candidate function void f(long int i) { ^
Il existe une conversion de unsigned int vers int et vers long int. Il n'y a pas de priorité dans les conversions, le compilateur ne sait pas quelle conversion choisir et donc quelle fonction appeler. L'appel est ambuigu (“call to 'f' is ambiguous”), il trouve deux fonctions candidate (“candidate function”).
La méthode qui permet au compilateur de trouver la fonction correspondant à une appel s'appelle la résolution des noms (name lookup)
Comme cela a déjà été expliqué, certains types, dont les littérales chaînes (et plus généralement les pointeurs), sont convertissable automatiquement en booléen. Si on écrit la surcharge suivante :
void foo(bool) { std::cout << "f(bool)" << std::endl; } void foo(string const&) { std::cout << "f(string)" << std::endl; } foo("abc");
Ce code ne va pas afficher f(string)
, mais f(bool)
. Si on ajoute une fonction f(const char*)
et sera appelée en premier. La raison est que la littérale chaîne est de type const char*
, les fonctions seront appelée dans l'ordre suivant :
Donc attention lorsque vous écrivez une fonction qui prend bool, elle peut prendre aussi n'importe quel pointeur.
Solution C++14 : écrire “abc”s pour créer une littérale de type string.
Détailler le name lookup
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|