Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
surcharge_fonctions [2016/08/10 02:22] gbdivers |
surcharge_fonctions [2016/08/30 12:59] (Version actuelle) gbdivers |
||
---|---|---|---|
Ligne 2: | Ligne 2: | ||
^ [[references|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[fonctions_generiques|Chapitre suivant]] ^ | ^ [[references|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[fonctions_generiques|Chapitre suivant]] ^ | ||
- | ====== [Aller plus loin] La surcharge de fonctions et résolution des noms ====== | + | ====== La surcharge de fonctions et résolution des noms ====== |
===== Plusieurs fonctions avec le même nom ===== | ===== Plusieurs fonctions avec le même nom ===== | ||
Ligne 105: | Ligne 105: | ||
L'étude des opérateurs et leur surcharge est suffisamment important pour être détaillé dans un chapitre dédié, dans la partie sur la programmation orientée objet. | L'étude des opérateurs et leur surcharge est suffisamment important pour être détaillé dans un chapitre dédié, dans la partie sur la programmation orientée objet. | ||
+ | |||
+ | |||
+ | ==== Cas particulier des references ==== | ||
+ | |||
+ | Vous avez vu dans le chapitre précédent qu'il existe plusieurs types de passage de valeurs dans une fonction : par valeur ou par références (constante ou non, //lvalue// ou //rvalue//). | ||
+ | |||
+ | Le point important a retenir est quelle type de paramètre accepte quel type d'argument. Cela était résumé dans le tableau suivant : | ||
+ | |||
+ | ^ Passage ^ lvalue ^ rvalue ^ | ||
+ | | Par valeur | oui | oui | | ||
+ | | Référence constante | oui | oui | | ||
+ | | Référence | oui | **non** | | ||
+ | | Rvalue-reference | **non** | oui | | ||
+ | |||
+ | Il est possible de surcharger des fonctions pour écrire du code spécifique, selon si la fonction est appellee avec une //lvalue// (une variable) ou une //rvalue// (un temporaire). | ||
+ | |||
+ | Mais pour éviter les ambiguïtés lors de l'appel de fonction, il ne faut pas écrire deux fonctions qui acceptent le même type de valeurs. Par exemple, si vous écrivez : | ||
+ | |||
+ | <code cpp> | ||
+ | void f(int i); // passage par valeur | ||
+ | void f(int && i); // passage par rvalue-reference | ||
+ | </code> | ||
+ | |||
+ | Si vous appelez cette fonction avec une //lvalue//, il n'y aura pas d'ambiguïté (seul le passage par valeur sera valide). Si vous appelez avec une //rvalue//, il y a ambiguïté (les deux versions de la fonction acceptent les //rvalues//). | ||
+ | |||
+ | Il y a donc que trois approches possibles : | ||
+ | |||
+ | 1. Si vous ne voulez pas écrire de code spécifique //lvalue// vs //rvalue// et que la copie n'est pas un problème (par exemple pour les types fondamentaux comme ''int'', ''double'', etc.), alors vous utilisez le passage par valeur. | ||
+ | |||
+ | <code cpp> | ||
+ | #include <iostream> | ||
+ | |||
+ | void f(int i) { | ||
+ | std::cout << "f(int i): " << i << std::endl; | ||
+ | } | ||
+ | |||
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | f(i); | ||
+ | f(456); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | f(int i): 123 | ||
+ | f(int i): 456 | ||
+ | </code> | ||
+ | |||
+ | 2. Si vous ne voulez pas écrire de code spécifique //lvalue// vs //rvalue// et que la copie est un problème (par exemple pour les classes comme ''std::string'', ''std::vector'', etc.), alors vous utilisez le passage par référence constante. | ||
+ | |||
+ | <code cpp> | ||
+ | #include <iostream> | ||
+ | #include <string> | ||
+ | |||
+ | void f(std::string const& s) { | ||
+ | std::cout << "f(std::string const& s): " << s << std::endl; | ||
+ | } | ||
+ | |||
+ | int main() { | ||
+ | std::string s { "hello" }; | ||
+ | f(s); | ||
+ | f("world"); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | f(std::string const& s): hello | ||
+ | f(std::string const& s): world | ||
+ | </code> | ||
+ | |||
+ | 3. Si vous voulez écrire de code spécifique //lvalue// vs //rvalue//, alors vous utilisez le passage par référence non constante (c'est a dire que vous écrivez deux fonctions surchargées, qui acceptent une //lvalue-reference// et une //rvalue-reference//). | ||
+ | |||
+ | <code cpp> | ||
+ | #include <iostream> | ||
+ | |||
+ | void f(int & i) { | ||
+ | std::cout << "f(int & i): " << i << std::endl; | ||
+ | } | ||
+ | |||
+ | void f(int && i) { | ||
+ | std::cout << "f(int && i): " << i << std::endl; | ||
+ | } | ||
+ | |||
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | f(i); | ||
+ | f(456); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | affiche : | ||
+ | |||
+ | <code> | ||
+ | f(int & i): 123 | ||
+ | f(int && i): 456 | ||
+ | </code> | ||
+ | |||
+ | Ce type de surcharge de fonction sera particulièrement intéressant lorsque vous concevrez vos propres classes, puisque cela permet d'optimiser la gestion des données internes, selon le type de valeurs utilisées. C'est ce qui est fait dans la bibliothèque standard. Par exemple, pour ''std::vector'', vous pouvez voir dans la documentation ([[http://en.cppreference.com/w/cpp/container/vector/vector|std::vector::vector]]) : | ||
+ | |||
+ | <code> | ||
+ | vector( const vector& other ); (5) | ||
+ | vector( vector&& other ) (6) | ||
+ | </code> | ||
Ligne 130: | Ligne 237: | ||
La résolution des noms est donc le processus qui permet au compilateur de déterminer ce que signifie chacun de ces identifiants. | La résolution des noms est donc le processus qui permet au compilateur de déterminer ce que signifie chacun de ces identifiants. | ||
+ | |||
+ | Notez que ce chapitre se limite aux cas simple de resolution de noms. Il existe des regles supplementaires pour les fonctions génériques (//template//), les classes. Ces règles seront vue dans les cours correspondants. | ||
==== Analyse sequentielle ==== | ==== Analyse sequentielle ==== | ||
Ligne 373: | Ligne 482: | ||
Dans ce code, l'argument de type ''float'' est automatiquement promu en type ''double'', puis la seconde version de la fonction ''f'' est appelee. | Dans ce code, l'argument de type ''float'' est automatiquement promu en type ''double'', puis la seconde version de la fonction ''f'' est appelee. | ||
+ | |||
+ | |||
+ | ==== Promotion et conversion ==== | ||
Si vous testez la même chose avec un argument de type ''long int'' : | Si vous testez la même chose avec un argument de type ''long int'' : | ||
Ligne 395: | Ligne 507: | ||
</code> | </code> | ||
- | Pourquoi dans le premier cas, le compilateur arrive à gérer la conversion implicite de ''float'' vers ''double'', mais n'arrive pas a gerer ''long int'' ? | + | Pourquoi, dans le premier cas, le compilateur arrive à gérer la conversion implicite de ''float'' vers ''double'', mais n'arrive pas a gerer ''long int'' ? |
- | La raison est qu'il existe plusieurs niveaux de conversion implicite : | + | La raison est qu'il existe plusieurs niveaux de conversion implicite, dans l'ordre de priorité suivant : |
* aucune conversion ; | * aucune conversion ; | ||
Ligne 405: | Ligne 517: | ||
Dans le cas de l'appel de ''f(1)'', le compilateur a le choix entre deux fonctions : appeler la fonction ''f(int)'' sans faire de conversion et appeler ''f(double)'' en faisant une conversion. La première est prioritaire par rapport à la seconde, il n'y a pas d'ambiguïté. Idem pour l'appel de ''f(1.0)'', qui appelle ''f(double)'' sans conversion. | Dans le cas de l'appel de ''f(1)'', le compilateur a le choix entre deux fonctions : appeler la fonction ''f(int)'' sans faire de conversion et appeler ''f(double)'' en faisant une conversion. La première est prioritaire par rapport à la seconde, il n'y a pas d'ambiguïté. Idem pour l'appel de ''f(1.0)'', qui appelle ''f(double)'' sans conversion. | ||
- | Dans le cas de l'appel de ''f(1.0f)'', le compilateur a le choix entre faire une promotion de ''float'' vers ''double'' pour appeler ''f(double)'' ou faire une conversion de ''float'' vers ''int'' pour appeler ''f(int)''. La promotion a priorité sur la conversion et ''f(double)'' est appellee sans ambiguïté. | + | Dans le cas de l'appel de ''f(1.0f)'', le compilateur a le choix entre faire une **promotion** de ''float'' vers ''double'' pour appeler ''f(double)'' ou faire une **conversion** de ''float'' vers ''int'' pour appeler ''f(int)''. La promotion est prioritaire sur la conversion et ''f(double)'' est appellee sans ambiguïté. |
Pour le dernier cas, l'appel de ''f(1L)'', le compilateur a le choix entre deux conversions : ''long int'' vers ''int'' et ''long int'' vers ''double''. Ce sont deux conversions, donc avec le même niveau de propriété, le compilateur ne peut pas décider laquelle choisir : il y a ambiguite. | Pour le dernier cas, l'appel de ''f(1L)'', le compilateur a le choix entre deux conversions : ''long int'' vers ''int'' et ''long int'' vers ''double''. Ce sont deux conversions, donc avec le même niveau de propriété, le compilateur ne peut pas décider laquelle choisir : il y a ambiguite. | ||
+ | La question est donc de savoir quand une conversion implicite est une promotion ou non. Le détail des promotions autorisees est donnée dans la documentation : [[http://en.cppreference.com/w/cpp/language/implicit_conversion#Numeric_promotions|Les promotions numériques]]. | ||
+ | Pour simplifier, retenez les promotions suivantes : | ||
+ | * la promotions d'un entier plus petit que ''int'' (''char'' ou ''short'') en ''int'' ; | ||
+ | * la promotion de ''bool'' en ''int'' ; | ||
+ | * la promotion de ''float'' en ''double''. | ||
+ | La promotion de ''bool'' en ''int'' est un reliquat du langage C, qui ne possédait pas de type ''bool''. Le type ''int'' était alors utilisé pour représenter un booléen, avec une valeur nulle pour représenter ''false'' et une valeur non nulle pour représenter ''true''. | ||
+ | Notez aussi que la conversion implicite d'une énumération à portée globale (//unscoped enum//) en entier est également une promotion. Voir [[enum_class|]]. | ||
- | 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 : | + | <note warning>**Pointeurs et booléens** |
- | <code> | + | Les pointeurs nus sont des types qui permettent de manipuler directement la mémoire en bas niveau. C'est une fonctionnalité avancée du C++, que vous verrez en détail plus tard, mais vous en avez déjà rencontré dans ce cours : les chaines littérales. |
- | 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. | + | |
- | </code> | + | |
- | + | ||
- | Ce qui signifie qu'il ne trouve aucune fonction correspond à l'appel de f("une chaine"), mais qu'il a 2 candidats (2 fonctions qui ont le même nom) mais sans conversion possible ("no known conversion"). | + | |
- | + | ||
- | Au contraire, dans certain cas, il aura plusieurs possibilités, 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 : | + | |
- | + | ||
- | <code> | + | |
- | void f() { | + | |
- | std::cout << "première fonction f" << std::endl; | + | |
- | } | + | |
- | + | ||
- | void f() { | + | |
- | std::cout << "seconde fonction f" << std::endl; | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | produit le message : | + | |
- | + | ||
- | <code> | + | |
- | main.cpp:7:6: error: redefinition of 'f' | + | |
- | void f() { | + | |
- | ^ | + | |
- | main.cpp:3:6: note: previous definition is here | + | |
- | void f() { | + | |
- | ^ | + | |
- | </code> | + | |
- | + | ||
- | 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 : | + | |
<code cpp> | <code cpp> | ||
- | #include <iostream> | + | auto str = "hello, world"; // type "pointeur" : const char* |
- | + | ||
- | 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 | + | |
- | } | + | |
</code> | </code> | ||
- | affiche le message d'erreur : | + | Le problème avec les pointeurs est qu'ils sont convertissable en booléen, ce qui produire des comportements surprenants. Par exemple, si vous écrivez : |
- | + | ||
- | <code> | + | |
- | 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) { | + | |
- | ^ | + | |
- | </code> | + | |
- | + | ||
- | 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 candidates ("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) | + | |
- | + | ||
- | <note warning>Note sur bool | + | |
- | + | ||
- | 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 : | + | |
<code cpp> | <code cpp> | ||
Ligne 507: | Ligne 551: | ||
</code> | </code> | ||
- | Ce code ne va pas afficher ''f(string)'', mais ''f(bool)''. Si on ajoute une fonction ''f(const char*)'', elle 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 : | + | Ce code ne va pas afficher ''f(string)'', mais ''f(bool)''. |
- | * f(const char*) : par de conversion entre l'argument et le paramètre ; | + | Si vous ajoutez une fonction ''f(const char*)'', celle-ci sera appelée en premier. La raison est que la littérale chaîne est de type ''const char*'', les fonctions surchargées ont donc les priorités suivantes, dans l'ordre : |
- | * f(bool) : conversion automatique ; | + | |
- | * f(string) : conversion passant par une classe. | + | |
- | Donc attention lorsque vous écrivez une fonction qui prend bool, elle peut prendre aussi n'importe quel pointeur. | + | * ''f(const char*)'', puisqu'il n'y a pas de conversion nécessaire entre l'argument et le paramètre ; |
+ | * ''f(bool)'', puisque cela necessite une simple conversion implicite d'un pointeur en ''bool'' ; | ||
+ | * ''f(std::string)'', puisque cela nécessite la construction d'une classe complexe. | ||
- | Solution C++14 : écrire "abc"s pour créer une littérale de type string. | + | Faites attention lorsque vous écrivez une fonction qui prend un paramètre de type ''bool'', celle-ci pourra également être appellee avec un argument de type pointeur, en particulier une littérale chaine. |
- | + | ||
- | __Détailler le name lookup__ | + | |
</note> | </note> | ||
- | |||
- | |||
- | |||
- | adl, cas particulier namespace, class, template... | ||
^ [[references|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[fonctions_generiques|Chapitre suivant]] ^ | ^ [[references|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[fonctions_generiques|Chapitre suivant]] ^ |