Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
surcharge_fonctions [2016/07/31 09:55] 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 resolution 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 ===== | ||
- | Le nom d'une fonction est le premier indicateur du rôle d'une fonction pour les utilisateurs de cette fonction. Il est donc important de donner un nom qui exprime le mieux ce rôle. Mais comment faire si vous souhaitez avoir plusieurs fonctions qui exécute la même tache, mais sur des types différents ? | + | Le nom d'une fonction est le premier indicateur du rôle d'une fonction pour les utilisateurs de cette fonction. Il est donc important de donner un nom qui exprime le mieux ce rôle. Mais comment faire si vous souhaitez avoir plusieurs fonctions qui exécute la même tâche, mais sur des types différents ? |
Par exemple, si vous souhaitez créer une fonction ''add'' pour additionner des entiers ou des réels. Une première solution est de donner des noms différents aux fonctions. | Par exemple, si vous souhaitez créer une fonction ''add'' pour additionner des entiers ou des réels. Une première solution est de donner des noms différents aux fonctions. | ||
Ligne 15: | Ligne 15: | ||
</code> | </code> | ||
- | Note : Pour les operateurs binaires, il est classique d'utiliser les noms ''lhs'' pour //left hand side// (côté gauche) et //right hand side// (côté droit). | + | Note : Pour les opérateurs binaires, il est classique d'utiliser les noms ''lhs'' pour //left hand side// (côté gauche) et //right hand side// (côté droit). |
- | C'est une approche possible, mais si vous pensez au nombre de types que vous avez vu jusque maintenant, vous comprendrez facilement que cela va vite devenir compliqué. (Mais pas impossible, puisque c'est ce que l'on fait dans certains langages de programmation, comme le C). | + | C'est une approche possible, mais si vous pensez au nombre de types que vous avez vu jusqu'à maintenant, vous comprendrez facilement que cela va vite devenir compliqué. (Mais pas impossible, puisque c'est ce que l'on fait dans certains langages de programmation, comme le C). |
==== La surcharge de fonctions ==== | ==== La surcharge de fonctions ==== | ||
Ligne 35: | Ligne 35: | ||
</code> | </code> | ||
- | La surcharge de fonction consiste a definir plusieurs fonctions avec le meme nom, mais des signatures differentes. Lors de l'appel de ces fonctions (en utilisant le nom de la fonction donc), le compilateur cherchera la signature qui s'adaptera le mieux aux types des arguments. (Vous voyez ici, encore, l'importance des types en C++). | + | La surcharge de fonction consiste à definir plusieurs fonctions avec le meme nom, mais des signatures differentes. Lors de l'appel de ces fonctions (en utilisant le nom de la fonction donc), le compilateur cherchera la signature qui s'adaptera le mieux aux types des arguments. (Vous voyez ici, encore, l'importance des types en C++). |
- | Pour la fonction ''add'' precedente : | + | Pour la fonction ''add'' précédente : |
<code cpp> | <code cpp> | ||
Ligne 58: | Ligne 58: | ||
==== Fonctions génériques ==== | ==== Fonctions génériques ==== | ||
- | Une solution alternative pour cette problematique est d'utiliser une fonction generique. Une fonction generique est simplement une fonction qui n'est pas ecrite pour un type de parametre en particulier, mais utilise l'inference de type pour determiner les types des parametres. | + | Une solution alternative pour cette problématique est d'utiliser une fonction générique. Une fonction générique est simplement une fonction qui n'est pas écrite pour un type de paramètre en particulier, mais utilise l'inférence de type pour déterminer les types des parametres. |
- | Par exemple, pour la fonction ''add'', il serait possible d'ecrire : | + | Par exemple, pour la fonction ''add'', il serait possible d'écrire : |
<code cpp> | <code cpp> | ||
Ligne 67: | Ligne 67: | ||
</code> | </code> | ||
- | Vous verrez dans le prochain chapitre comment creer des fonctions generiques, ne vous pre-occupez pas trop de la syntaxe pour le moment. Sachez simplement que le type ''T'' dans les parametres de la fonction sera remplace par un type concret (''int'', ''double'', etc.) lors de l'appel de la fonction. | + | Vous verrez dans le prochain chapitre comment creer des fonctions génériques, ne vous préoccupez pas trop de la syntaxe pour le moment. Sachez simplement que le type ''T'' dans les paramètres de la fonction sera remplacé par un type concret (''int'', ''double'', etc.) lors de l'appel de la fonction. |
- | Le point important est que pour la surcharge de fonction, vous devez ecrire une fonction pour chaque type que la fonction pourra accepter. Et pour les fonctions generique, il est necessaire d'ecrire qu'une seule fois la fonction. | + | Le point important est que pour la surcharge de fonction, vous devez écrire une fonction pour chaque type que la fonction pourra accepter. Et pour les fonctions générique, il est nécessaire d'écrire qu'une seule fois la fonction. |
- | (La surcharge de fonctions et les fonctions generiques sont des concepts independants, ils peuvent etre utiliser ensemble pour creer des surcharges de fonctions generiques). | + | (La surcharge de fonctions et les fonctions génériques sont des concepts indépendants, ils peuvent être utiliser ensemble pour creer des surcharges de fonctions génériques). |
==== D'autres exemple de surcharge de fonctions ==== | ==== D'autres exemple de surcharge de fonctions ==== | ||
- | Un autre exemple de surcharge de fonction, que vous connaissez bien... sans le savoir. Lorsque vous ecrivez : | + | Un autre exemple de surcharge de fonction, que vous connaissez bien... sans le savoir. Lorsque vous écrivez : |
<code cpp> | <code cpp> | ||
Ligne 81: | Ligne 81: | ||
</code> | </code> | ||
- | En fait, l'operateur ''<<'' est une fonction qui prend deux parametres : le flux de sortie (''std::cout'' dans ce code) et une valeur (''i'' par exemple dans ce code). Le code precedent peut donc se traduire : | + | En fait, l'opérateur ''<<'' est une fonction qui prend deux paramètres : le flux de sortie (''std::cout'' dans ce code) et une valeur (''i'' par exemple dans ce code). Le code précédent peut donc se traduire : |
<code cpp> | <code cpp> | ||
Ligne 87: | Ligne 87: | ||
</code> | </code> | ||
- | Ou encore, en faisant apparaitre l'expression intermediaire : | + | Ou encore, en faisant apparaître l'expression intermédiaire : |
<code cpp> | <code cpp> | ||
Ligne 94: | Ligne 94: | ||
</code> | </code> | ||
- | L'operateur ''<<'' est en fait une surcharge de fonction, qui accepte de nombreux types comme second parametre. Lorsque le compilateur rencontre l'expression ''std::cout << i'', il determine quel est le type de ''i'', puis appelle l'operateur ''<<'' adequate (sans conversion si possible, avec la conversion la plus simple sinon). | + | L'opérateur ''<<'' est en fait une surcharge de fonction, qui accepte de nombreux types comme second paramètre. Lorsque le compilateur rencontre l'expression ''std::cout << i'', il determine quel est le type de ''i'', puis appelle l'opérateur ''<<'' adéquate (sans conversion si possible, avec la conversion la plus simple sinon). |
- | Et c'est la meme chose pour les autres operateurs que vous connaissez (''+'', ''*'', etc.) | + | Et c'est la même chose pour les autres opérateurs que vous connaissez (''+'', ''*'', etc.) |
<code cpp> | <code cpp> | ||
Ligne 104: | Ligne 104: | ||
</code> | </code> | ||
- | L'etude des operateurs et leur surcharge est suffisamment important pour etre detaille dans un chapitre dedie, dans la partie sur la programmation orientee 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 129: | Ligne 236: | ||
* ''endl'' | * ''endl'' | ||
- | La resolution des noms est donc le processus qui permet au compilateur de determiner 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 ==== | ||
- | Vous avez vu au debut de ce cours qu'en programmation imperative, les instructions etaient executees les unes apres les autres. (dans [[hello_world|]]). Le compilateur fonctionne de la meme facon : il lit le code ligne par ligne, de facon purement sequentielle. Lorsqu'il lit une ligne, il utilise les informations qu'il a appris en lisant les lignes precedentes. | + | Vous avez vu au début de ce cours qu'en programmation impérative, les instructions étaient exécutées les unes après les autres (dans [[hello_world|]]). Le compilateur fonctionne de la même façon : il lit le code ligne par ligne, de façon purement séquentielle. Lorsqu'il lit une ligne, il utilise les informations qu'il a appris en lisant les lignes précédentes. |
C'est pour cette raison que vous avez vu qu'il fallait definir une fonction avant de l'utiliser. | C'est pour cette raison que vous avez vu qu'il fallait definir une fonction avant de l'utiliser. | ||
Ligne 141: | Ligne 250: | ||
int main() { | int main() { | ||
- | f(); // ok, le compilateur connait deja f | + | f(); // ok, le compilateur connaît déjà f |
- | g(); // erreur, le compilateur ne connait pas encore g | + | g(); // erreur, le compilateur ne connaît pas encore g |
} | } | ||
Ligne 148: | Ligne 257: | ||
</code> | </code> | ||
- | Une exception a cette regle : le compilateur connait un certain nombre d'identifiants par defaut, definis dans la norme C++. Ces identifiants sont reserves, il vous est interdit de les utiliser. Ce sont les mots-cles du langage. Vous connaissez deja : | + | Une exception à cette règle : le compilateur connaît un certain nombre d'identifiants par défaut, définis dans la norme C++. Ces identifiants sont réservés, il vous est interdit de les utiliser. Ce sont les mots-clés du langage. Vous connaissez deja : |
- | * ''auto'' : inference de type ; | + | * ''auto'' : inférence de type ; |
- | * ''bool'' : type booleen ; | + | * ''bool'' : type booléen ; |
* ''char'' : type d'entier de taille la plus petite (''sizeof(char) == 1''), representant un caractere ; | * ''char'' : type d'entier de taille la plus petite (''sizeof(char) == 1''), representant un caractere ; | ||
* ''class'' : pour definir une structure de donnees (classe) ; | * ''class'' : pour definir une structure de donnees (classe) ; | ||
- | * ''const'' : constant (vous avez vu ce mot-cle pour les variables, mais il sera utilise aussi pour les fonctions membres) ; | + | * ''const'' : constant (vous avez vu ce mot-clé pour les variables, mais il sera utilisé aussi pour les fonctions membres) ; |
- | * ''constexpr'' : expression constante (evaluee lors de la compilation si possible) ; | + | * ''constexpr'' : expression constante (évaluée lors de la compilation si possible) ; |
- | * ''decltype'' : inference de type ; | + | * ''decltype'' : inférence de type ; |
- | * ''double'' : type de nombre a virgule flottante, generalement sur 64 bits ; | + | * ''double'' : type de nombre à virgule flottante, généralement sur 64 bits ; |
* ''enum'' : enumeration ; | * ''enum'' : enumeration ; | ||
* ''false'' : valeur booleenne "faux" ; | * ''false'' : valeur booleenne "faux" ; | ||
- | * ''float'' : type de nombre a virgule flottante, generalement sur 32 bits ; | + | * ''float'' : type de nombre à virgule flottante, généralement sur 32 bits ; |
- | * ''int'' : type d'entier, intermediaire entre ''short int'' et ''long int'', generalement sur 32 bits ; | + | * ''int'' : type d'entier, intermédiaire entre ''short int'' et ''long int'', généralement sur 32 bits ; |
- | * ''long'' : modificateur de type, permet d'utiliser des entiers (''int'') et reels (''double'') de plus grande taille ; | + | * ''long'' : modificateur de type, permet d'utiliser des entiers (''int'') et réels (''double'') de plus grande taille ; |
- | * ''operator'' : pour definir un operateur (fonction particulier, comme ''+'', ''*'', ''<<'', etc.) ; | + | * ''operator'' : pour definir un opérateur (fonction particulière, comme ''+'', ''*'', ''<<'', etc.) ; |
* ''return'' : retourne une valeur et termine une fonction ; | * ''return'' : retourne une valeur et termine une fonction ; | ||
* ''short'' : modificateur de type, permet d'utiliser des entiers (''int'') de plus petite taille ; | * ''short'' : modificateur de type, permet d'utiliser des entiers (''int'') de plus petite taille ; | ||
- | * ''signed'' : modificateur de type, pour creer une entier signe ; | + | * ''signed'' : modificateur de type, pour creer une entier signé ; |
- | * ''sizeof'' : operateur permettant de connaitre la taille en memoire d'un type ou d'une variable ; | + | * ''sizeof'' : opérateur permettant de connaître la taille en mémoire d'un type ou d'une variable ; |
- | * ''struct'' : pour definir une structure de donnees ; | + | * ''struct'' : pour definir une structure de données ; |
- | * ''template'' : pour definir une fonction ou une classe generique ; | + | * ''template'' : pour definir une fonction ou une classe générique ; |
- | * ''true'' : valeur booleenne "vrai" ; | + | * ''true'' : valeur booléenne "vrai" ; |
* ''typedef'' : ancienne syntaxe pour definir un alias de type ; | * ''typedef'' : ancienne syntaxe pour definir un alias de type ; | ||
* ''unsigned'' : modificateur de type, pour creer une entier non signe ; | * ''unsigned'' : modificateur de type, pour creer une entier non signe ; | ||
Ligne 176: | Ligne 285: | ||
* ''void'' : indique qu'une fonction ne retourne pas de valeur. | * ''void'' : indique qu'une fonction ne retourne pas de valeur. | ||
- | Certain de ces mots-cles ont des utilisations que vous n'avez pas encore vues et il existe d'autres mots-cles que vous verrez par la suite. La liste complete des mots-cles est indiquee dans la documentation : [[http://en.cppreference.com/w/cpp/keyword|C++ keywords]]. | + | Certain de ces mots-clés ont des utilisations que vous n'avez pas encore vues et il existe d'autres mots-clés que vous verrez par la suite. La liste complete des mots-cles est indiquee dans la documentation : [[http://en.cppreference.com/w/cpp/keyword|C++ keywords]]. |
- | ==== Directive de pre-processeur ==== | + | ==== Directive de préprocesseur ==== |
- | Dans le code du programme "hello world", la premiere instruction rencontree par le compilateur est la directive de pré-processeur ''#include''. Il existe plusieurs directives de compilation, elles seront expliquees dans un chapitre dedie. | + | Dans le code du programme "hello world", la première instruction rencontrée par le compilateur est la directive de préprocesseur ''#include''. Il existe plusieurs directives de compilation, elles seront expliquées dans un chapitre dédié. |
- | Une directive commence toujours pas un diese ''#''. La directive ''#include'' permet d'utiliser les fonctionnalites declarees dans un fichier d'en-tete (de la bibliotheque standard pour le moment, mais vous verrez plus tard qu'il est possible de creer ses propres fichiers d'en-tete). | + | Une directive commence toujours par un dièse ''#''. La directive ''#include'' permet d'utiliser les fonctionnalités déclarées dans un fichier d'en-tete (de la bibliotheque standard pour le moment, mais vous verrez plus tard qu'il est possible de creer ses propres fichiers d'en-tête). |
- | Il est possible de mettre la directive ''#include'' a n'importe quel endroit (en dehors des fonctions), mais la regle sequentielle s'applique : vous ne pouvez pas utiliser une fonctionnalite d'un fichier d'en-tete avant d'avoir include ce fichier d'en-tete. | + | Il est possible de mettre la directive ''#include'' a n'importe quel endroit (en dehors des fonctions), mais la règle séquentielle s'applique : vous ne pouvez pas utiliser une fonctionnalité d'un fichier d'en-tete avant d'avoir inclue ce fichier d'en-tete. |
<code cpp> | <code cpp> | ||
Ligne 200: | Ligne 309: | ||
</code> | </code> | ||
- | Pour faciliter la lecture et eviter les erreurs, il est classique de placer les directives ''#include'' au debut du code. Cette regle sera utilisee dans ce cours. | + | Pour faciliter la lecture et éviter les erreurs, il est classique de placer les directives ''#include'' au debut du code. Cette règle sera utilisée dans ce cours. |
==== Nom qualifié et non-qualifié ==== | ==== Nom qualifié et non-qualifié ==== | ||
- | Dans le code du programme "hello world", vous avez vu comment le compilateur va interpreter les premiers identifiants : la directive ''#include'' et le mot-cle ''int''. Il va egalement comprendre que l'identifiant ''main'' est la definition d'une nouvelle fonction et qu'il ne connait pas encore ce nom, il va donc l'ajouter a sa liste des noms connus. | + | Dans le code du programme "hello world", vous avez vu comment le compilateur va interpreter les premiers identifiants : la directive ''#include'' et le mot-cle ''int''. Il va également comprendre que l'identifiant ''main'' est la définition d'une nouvelle fonction et qu'il ne connaît pas encore ce nom, il va donc l'ajouter a sa liste des noms connus. |
- | Le compilateur arrive ensuite a l'identifiant ''std''. Il sait que les doubles deux-points ''::'' correspondent a l'operateur de portee, qui est utilise avec les espaces de noms, les classes ou les enumerations par exemple. L'espace de noms ''std'' est defini dans le fichier d'en-tete ''iostream'', donc le compilateur connait deja cet identifiant quand il arrive a l'instruction ''std::cout''. | + | Le compilateur arrive ensuite à l'identifiant ''std''. Il sait que les doubles deux-points ''::'' correspondent à l'opérateur de portée, qui est utilisé avec les espaces de noms, les classes ou les énumérations par exemple. L'espace de noms ''std'' est défini dans le fichier d'en-tête ''iostream'', donc le compilateur connaît déjà cet identifiant quand il arrive a l'instruction ''std::cout''. |
- | Un identifiant precede d'une portee (une classe, un espace de noms, enumeration, etc.) est appelee "nom qualifie" (//qualified name//), sinon il est appelle "nom non-qualifie" (//unqualified name//). Ainsi, ''std::cout'' et ''std::endl'' sont des noms qualifies, alors que ''main'' et ''int'' ne sont pas qualifie. | + | Un identifiant précédé d'une portée (une classe, un espace de noms, énumération, etc.) est appelee "nom qualifié" (//qualified name//), sinon il est appelle "nom non-qualifié" (//unqualified name//). Ainsi, ''std::cout'' et ''std::endl'' sont des noms qualifiés, alors que ''main'' et ''int'' ne sont pas qualifiés. |
- | La portee permet d'utiliser un meme identifiant dans plusieurs definitions, a partir du moment ou ces identifiants sont dans des portees differentes. Par exemple, la fonction ''size'' peut correspondre a des fonctions differentes, dans ces classes differentes : ''std::string::size'', ''std::vector::size'', etc. (Il est possible d'avoir plusieurs portees les unes dans les autres). | + | La portée permet d'utiliser un même identifiant dans plusieurs définitions, à partir du moment ou ces identifiants sont dans des portées différentes. Par exemple, la fonction ''size'' peut correspondre à des fonctions différentes, dans ces classes differentes : ''std::string::size'', ''std::vector::size'', etc. (Il est possible d'avoir plusieurs portées les unes dans les autres). |
Dans le chapitre [[hello_world|]], vous avez vu qu'il existe plusieurs syntaxes pour utiliser un espace de noms, en particulier une syntaxe avec ''using namespace''. Cette syntaxe permet d'indiquer au compilateur qu'il peut rechercher n'importe quel identifiant aussi dans l'espace de noms ''std''. | Dans le chapitre [[hello_world|]], vous avez vu qu'il existe plusieurs syntaxes pour utiliser un espace de noms, en particulier une syntaxe avec ''using namespace''. Cette syntaxe permet d'indiquer au compilateur qu'il peut rechercher n'importe quel identifiant aussi dans l'espace de noms ''std''. | ||
Ligne 227: | Ligne 336: | ||
</code> | </code> | ||
- | Dans ce cas, il ne connait qu'un seul identifiant qui peut correspondre : ''std::cout'' definie dans ''iostream''. | + | Dans ce cas, il ne connaît qu'un seul identifiant qui peut correspondre : ''std::cout'' definie dans ''iostream''. |
Mais si vous souhaitez ajouter une fonction ''cout'' : | Mais si vous souhaitez ajouter une fonction ''cout'' : | ||
Ligne 259: | Ligne 368: | ||
</code> | </code> | ||
- | Le compilateur trouve en effet deux definitions pouvant correspondre a ''cout'' : la fonction ''cout'' definie dans le code et le flux de sortie ''std::cout'' definie dans ''iostream''. Ces deux identifiants sont valides et le compilateur ne peut determiner lequel il doit utiliser, il y a ambiguite (//reference to 'cout' is ambiguous//). | + | Le compilateur trouve en effet deux définitions pouvant correspondre à ''cout'' : la fonction ''cout'' définie dans le code et le flux de sortie ''std::cout'' définie dans ''iostream''. Ces deux identifiants sont valides et le compilateur ne peut déterminer lequel il doit utiliser, il y a ambiguïté (//reference to 'cout' is ambiguous//). |
- | Notez bien que seul l'instruction ''cout'' dans la fonction ''main'' est ambigue. L'instruction ''std::cout'' dans la fonction ''cout()'' est qualifiee, elle n'est pas ambigue. | + | Notez bien que seul l'instruction ''cout'' dans la fonction ''main'' est ambiguë. L'instruction ''std::cout'' dans la fonction ''cout()'' est qualifiée, elle n'est pas ambigue. |
- | L'utilisation de ''using namespace'' reduit l'interet des espaces de noms et peut poser des problemes de conflits dans les noms. L'utilisation de cette syntaxe est generalement limitee aux fichiers sources (vous verrez bientot la separation du code entre fichiers d'en-tete et fichiers source). | + | L'utilisation de ''using namespace'' réduit l'intérêt des espaces de noms et peut poser des problèmes de conflits dans les noms. L'utilisation de cette syntaxe est généralement limitée aux fichiers sources (vous verrez bientôt la séparation du code entre fichiers d'en-tête et fichiers source). |
- | ==== Regle de la definition unique et portee ==== | + | ==== Regle de la définition unique et portée ==== |
- | Pour interpreter un code, le compilateur maintient donc une liste des identifiants qu'il connait et ce qu'ils signifient. En C++, un **declaration** est une syntaxe qui dit au compilateur qu'un identifiant existe. Une **definition** est une declaration qui dit au compilateur a quoi correspond un identifiant. | + | Pour interpréter un code, le compilateur maintient donc une liste des identifiants qu'il connaît et ce qu'ils signifient. En C++, un **déclaration** est une syntaxe qui dit au compilateur qu'un identifiant existe. Une **définition** est une déclaration qui dit au compilateur a quoi correspond un identifiant. |
Pour utiliser un identifiant, il faut que : | Pour utiliser un identifiant, il faut que : | ||
- | * celui-ci soit definie et pas simplement declare ; | + | * celui-ci soit définie et pas simplement déclarée ; |
- | * la definition doit etre unique (ODR, //One Definition Rule//, regle de la definition unique). | + | * la définition doit être unique (ODR, //One Definition Rule//, regle de la definition unique). |
- | Par exemple, si vous definissez deux fois la meme fonction (meme nom et meme signature) : | + | Par exemple, si vous définissez deux fois la même fonction (même nom et même signature) : |
<code cpp> | <code cpp> | ||
Ligne 297: | Ligne 406: | ||
</code> | </code> | ||
- | La regle de la definition unique prend en compte la portee (les blocs de code, les espaces de noms, les classes, etc.). Vous pouvez donc utiliser plusieurs fois le meme identifiant, si c'est dans des portees differentes. | + | La règle de la définition unique prend en compte la portee (les blocs de code, les espaces de noms, les classes, etc.). Vous pouvez donc utiliser plusieurs fois le même identifiant, si c'est dans des portées différentes. |
<code cpp> | <code cpp> | ||
Ligne 326: | Ligne 435: | ||
- | ==== Resolution de la surcharge ==== | + | ==== Résolution de la surcharge ==== |
- | Lorsque le compilateur rencontre une fonction, il regarde dans la liste des identifiants qu'il connait pour trouver les fonctions definies avec le meme nom. Comme indique au debut de ce chapitre, il est possible d'avoir plusieurs fonctions qui possedent le meme nom, a partir du moment ou leur signature est differentes (la liste des types de leurs parametres). | + | Lorsque le compilateur rencontre une fonction, il regarde dans la liste des identifiants qu'il connaît pour trouver les fonctions définies avec le même nom. Comme indiqué au début de ce chapitre, il est possible d'avoir plusieurs fonctions qui possedent le meme nom, a partir du moment ou leur signature est différentes (la liste des types de leurs paramètres). |
- | Le compilateur doit donc determiner exactement quelle fonction appeler. Pour cela, il va regarder la liste des types des arguments utilises lors de l'appel de la fonction, puis selectionner la fonction la plus adaptee. | + | Le compilateur doit donc déterminer exactement quelle fonction appeler. Pour cela, il va regarder la liste des types des arguments utilisés lors de l'appel de la fonction, puis sélectionner la fonction la plus adaptée. |
- | Par exemple, si vous ecrivez deux fonctions, l'une qui prend un parametre de type ''int'' et l'autre qui prend un parametre de type ''double''. Si vous appellez cette fonction en passant une valeur de type entiere, la premiere version sera automatiquement appellee. Avec une valeur de type reelle, la seconde version sera appelllee. | + | Par exemple, si vous écrivez deux fonctions, l'une qui prend un paramètre de type ''int'' et l'autre qui prend un paramètre de type ''double''. Si vous appelez cette fonction en passant une valeur de type entière, la première version sera automatiquement appellée. Avec une valeur de type réelle, la seconde version sera appellée. |
<code cpp main.cpp> | <code cpp main.cpp> | ||
Ligne 358: | Ligne 467: | ||
</code> | </code> | ||
- | Dans ce code d'exemple, les types des arguments et de parametres correspondent parfaitement. Il n'y a aucune ambiguite sur les appels de fonction et le compilateur gere la resolution de la surcharge sans probleme. | + | Dans ce code d'exemple, les types des arguments et de paramètres correspondent parfaitement. Il n'y a aucune ambiguïté sur les appels de fonction et le compilateur gère la résolution de la surcharge sans problème. |
- | Mais la resolution de la surcharge peut egalement fonctionner lorsque les types ne correspondent pas parfaitement. Par exemple, si vous utiliser un argument de type ''float''. | + | Mais la résolution de la surcharge peut également fonctionner lorsque les types ne correspondent pas parfaitement. Par exemple, si vous utilisez un argument de type ''float''. |
<code cpp> | <code cpp> | ||
Ligne 374: | Ligne 483: | ||
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. | ||
- | Si vous testez la meme chose avec un argument de type ''long int'' : | + | |
+ | ==== Promotion et conversion ==== | ||
+ | |||
+ | Si vous testez la même chose avec un argument de type ''long int'' : | ||
<code cpp> | <code cpp> | ||
Ligne 380: | Ligne 492: | ||
</code> | </code> | ||
- | Dans ce cas, le compilateur va generer un message d'erreur ! | + | Dans ce cas, le compilateur va générer un message d'erreur ! |
<code> | <code> | ||
Ligne 395: | Ligne 507: | ||
</code> | </code> | ||
- | Pourquoi dans le premier cas, le compilateur arrive a gerer 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 403: | Ligne 515: | ||
* la conversion. | * la 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 premiere est prioritaire par rapport a la seconde, il n'y a pas d'ambiguite. 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 priorite sur la conversion et ''f(double)'' est appellee sans ambiguite. | + | 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 meme niveau de priopriete, le compilateur ne peut pas decider 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]] ^ |