Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
references [2016/06/29 02:48] gbdivers |
references [2018/08/19 16:23] (Version actuelle) gbdivers |
||
---|---|---|---|
Ligne 4: | Ligne 4: | ||
====== Copies, déplacements et indirections ====== | ====== Copies, déplacements et indirections ====== | ||
- | ===== Organisation de la mémoire ===== | + | Dans le chapitre précédent, vous avez vu comment échanger des informations avec une fonction, en utilisant les paramètres de fonction et le retour de fonction. |
- | ==== Vue d'ensemble ==== | + | Le type d’échange que vous avez vu s'appelle un passage par valeur. Cela consiste a partir de l'objet qui se trouve dans la fonction appelante, et d'en faire une copie qui sera utilisable dans la fonction appelée. |
- | <note>**Avant-propos** | + | <code cpp> |
+ | void f(int i) { | ||
+ | // i est une nouvelle "variable", accessible uniquement dans la fonction f | ||
+ | // et qui contient la même valeur que la variable j de la fonction g. | ||
+ | } | ||
- | Les explications donnees dans ce chapitre sont volontairement simplifiees, pour vous aider a comprendre le fonctionnement des fonctions et le passage d'arguments. Mais l'organisation et la gestion de la mémoire est | + | void g() { |
- | quelque chose de relativement complexe dans les details, en particulier parce que cela va dependre des interactions avec le systeme d'exploitation.</note> | + | const int j { 123 }; |
+ | f(j); | ||
+ | } | ||
+ | </code> | ||
- | La mémoire peut être vue comme un immense tableau d'octets. Dans ce tableau, l'index est l'adresse mémoire, chaque octet peut donc être identifié de manière unique par son adresse. | + | La valeur est copiée lors de l'appel de la fonction f, ce qui implique que les éventuelles modifications du paramètre ''i'' ne seront pas répercutées sur la variable ''j''. |
- | Il est possible de représenter les octets sous forme d'un tableau à deux dimensions, pour gagner de la place. Par exemple, dans la figure suivante, chaque octet est représenté par une case, un objet de type ''char'' sera représenté par une case aussi (en bleu), un objet de type ''int'' sera représenté par quatre cases (en vert) et un objet de type ''double'' par huit cases (en orange). | + | Il existe d'autres façon de transmettre une information dans une fonction, il convient donc de détailler d'abord les concepts de copie, de déplacement et d'indirection. |
- | {{ :memory.png |}} | + | ===== Copie et déplacement ===== |
- | Pour connaitre l'adresse memoire d'un objet (son indice dans le tableau), il faut utiliser l'operateur //address-of// ''&'' devant un identifiant de variable. Et pour rappel, pour connaitre la taille en memoire d'un objet, il faut utiliser l'operateur ''sizeof''. | + | ==== La copie d'objets ==== |
+ | |||
+ | La copie consiste donc a creer un nouvel objet, identique a un objet existant. Ces deux objets seront indépendants, c'est a dire que si l'un des objets est modifié ou détruit, l'autre objet ne sera pas modifie. | ||
<code cpp main.cpp> | <code cpp main.cpp> | ||
#include <iostream> | #include <iostream> | ||
- | + | ||
int main() { | int main() { | ||
const int i { 123 }; | const int i { 123 }; | ||
- | const auto address { &i }; | + | int j { i }; // j est une copie de i, elle contient la même valeur |
- | const auto size { sizeof(i) }; | + | std::cout << "i=" << i << ", j=" << j << std::endl; |
- | std::cout << std::hex << std::showbase; | + | j = 456; |
- | std::cout << "address: " << address << std::endl; | + | std::cout << "i=" << i << ", j=" << j << std::endl; |
- | std::cout << "size: " << size << std::endl; | + | |
} | } | ||
</code> | </code> | ||
Ligne 37: | Ligne 45: | ||
<code> | <code> | ||
- | address: 0x7fff1a3f51ac | + | i=123, j=123 |
- | size: 0x4 | + | i=123, j=456 |
</code> | </code> | ||
- | Une adresse memoire est souvent representee sous forme hexadecimale. | + | {{ :copie-move-1.png |}} |
- | <note warning>**Attention aux syntaxes** | + | Tous les objets ne sont pas copiables. Les types fondamentaux (''int'', ''double'', ''float'', etc) sont copiables, ainsi que la très grande majorité des classes de la bibliothèque standard. |
- | Vous avez deja rencontre un operateur ''&'' dans les chapitres precedents : l'operateur ET bit-a-bit. Vous verrez egalement que cet operateur peut representer aussi les references (qui sont un type d'indirections, que vous allez voir dans la suite de ce chapitre). | + | <note>Souvenez vous, cela a été abordé dans le chapitre [[collection2|]], la majorité des classes de la bibliothèque standard possèdent une sémantique de valeur et sont copiables. Un exemple de classe non copiable est ''std::unique_ptr''. Cela sera détaillé dans la partie sur la programmation objet.</note> |
- | Dans la meme facon, il existe l'operateur ''*'', qui peut representer une multiplication, un pointeur nu (un autre type d'indirection) ou un dereferencement (qui correspond a l'operation inverse de //address-of//, c'est a dire passer d'une adresse a une valeur). | ||
- | ^ ^ ''&'' ^ ''*'' ^ | + | ==== Le deplacement d'objets ==== |
- | | valeur @ valeur | ET bit-a-bit | multiplication | | + | |
- | | @ valeur | address-of | dereferencement | | + | |
- | | type @ nom | reference | pointeur | | + | |
- | Quand vous lisez un code, il faut donc bien faire attention a ce que represente chaque identifiant (variable, type, fonction, etc). | + | Le déplacement d'objets consiste a déplacer (//move//) un objet depuis une variable vers une autre. L'objet n'est pas modifié dans cette opération, il est conservé a l'identique. Cette opération peut être réalisée en utilisant la fonction ''std::move''. |
- | </note> | + | |
+ | <code cpp main.cpp> | ||
+ | #include <iostream> | ||
- | ==== Creation et destruction des objets ==== | + | int main() { |
+ | int i { 123 }; | ||
+ | int j { std::move(i) }; | ||
+ | std::cout << "j=" << j << std::endl; | ||
+ | } | ||
+ | </code> | ||
- | Les deux operations de base sur les objets ont deja ete vue : ce sont la creation et la destruction des objets. Comme vous l'avez deja vu, ces deux operations sont automatiques pour les variables locales, c'est donc l'approche a plus simple pour gerer les objets. | + | {{ :copie-move-2.png |}} |
- | Il est egalement possible de creer et detruire dynamiquement des objets, c'est a dire lorsque la creation et destruction des objets ne peut pas etre determinee a la compilation. C'est ce que fait par exemple ''std::vector'' lorsque vous changer sa taille lors de l'execution ou ''std::string'' lorsque vous modifiez le texte. | + | <note>Cette notion a aussi ete vu rapidement dans [[collection2|]] et sera detaille dans la partie sur la programmation objet.</note> |
- | Ces classes sont interessantes, puisqu'elles permettent de gerer la memoire dynamiquement, en conservant les avantages des variables locales pour gerer la memoire. Ce type de classe est appelle une "capsule RAII", vous verrez cela plus en detail dans la partie sur la programmation orientee objet. | + | Contrairement a la copie qui permet d'obtenir deux objets au final, le deplacement ne modifie pas le nombre d'objets, il y a toujours un seul objet valide apres l'operation. La variable qui contenait l'objet initialement contient ensuite un objet invalide qui ne doit jamais etre utilise (cela produirait un comportement indefini). La variable ne peut etre utilisee que pour lui affecter un nouvel objet. |
- | La bibliotheque standard offert de nombreuses classes RAII, et une approche moderne de la programmation C++ privilegera leur utilisation (ou la creation de nouvelles classes RAII dans le cas de fonctionnalites non fournies par la bibliotheque standard) plutot que la gestion manuelle des objets. | + | <code cpp main.cpp> |
+ | #include <iostream> | ||
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | int j { std::move(i) }; | ||
+ | std::cout << "j=" << j << std::endl; // interdit d'utiliser i ici | ||
+ | i = 456; | ||
+ | std::cout << "i=" << i << ", j=" << j << std::endl; // ok | ||
+ | } | ||
+ | </code> | ||
- | ==== Copie d'objets ==== | + | Tous les objets ne sont pas deplacable (//movable//). La copie et le deplacement sont independants, il est donc possible d'avoir des objets copiables et deplacables, des objets deplacables et non copiables, ou des objets non copiables et non deplacables. |
- | Chaque objet est donc represente en memoire par un bloc d'octets continues, avec une taille et une addresse. L'adresse est suffisante pour identifier chaque objet de maniere unique, il suffit donc de comparer les adresses memoires de deux objets pour savoir s'ils sont le meme objet ou non. | + | <note>Le deplacement est parfois considere comme une copie, et donc cela n'a pas de sens d'avoir un objet copiable, mais non deplacable. C'est possible syntaxiquement de faire cela, mais cela sera considere comme une erreur de conception dans ce cours.</note> |
- | La copie d'un objet consiste a creer un nouvel objet, identique a un objet existant. Ce nouvel objet sera identique en memoire, sauf qu'il aura une addresse differente. | + | En pratique, ce qui se passe reelement lors d'un deplacement est un peu complexe. Dans certains cas (par exemple avec les types fondamentaux), une copie sera realisee. Dans d'autres cas (par exemple avec ''std::string'' ou ''std::vector''), les donnees ne sont reellement pas copiees et le deplacement est plus performant que la copie. Dans tous les cas, vous pouvez retenir que le deplacement ne sera jamais plus couteux que la copie (au pire, il peut etre equivalent a une copie). |
+ | La fonction ''std::move'' ne realise donc pas a proprement parle un deplacement, mais dit au compilateur qu'un objet peut etre deplace. Libre a lui de realiser ou non un deplacement, si c'est possible. Mais dans d'autres cas, le compilateur peut decider par lui meme qu'un objet peut etre deplace et le fera automatiquement. (C'est une optimisation automatique, puisque le deplacement sera potentiellement plus performant que la copie). | ||
+ | <code cpp> | ||
+ | int f() { | ||
+ | int i { 123 }; | ||
+ | return i; // i ne sera plus utilise ensuite dans f et peut etre deplace | ||
+ | } | ||
+ | </code> | ||
- | copie = création d'un objet identique en mémoire. C'est-à-dire que les octets sont identiques, mais avec une adresse différente. On obtient 2 objets valides. | + | Dans ce code, la variable ''i'' ne sera plus utilisable apres le ''return'' (puisque la fonction sera terminee) et peut donc etre deplacee vers la fonction appelante. |
- | {{ :copie.png |}} | ||
- | Deux objets copiés sont indépendants. Si on modifie ou supprime l'un, l'autre n'est pas modifié. | + | ===== Les indirections ===== |
- | Tous les objets ne sont pas copiables. Les types fondamentaux (int, float, etc) sont par défaut copiable. Pour les objets plus complexes, cela dépendra de la sémantique que l'on veut donner à sa classe (sémantique de valeur). La majorité des classes de la STL (Standard Template Library) ont une sémantique de valeur. (ce sera détaillé dans la partie POO). | + | ==== La concept d'indirection ==== |
- | + | Avec une copie ou un deplacement, vous avez dans les deux cas une variable locale a la fonction qui contient un objet. Mais il peut etre interessant aussi de pouvoir manipuler un objet qui n'est pas dans la fonction, par exemple pour modifier un objet qui se trouve dans un autre fonction ou acceder a un objet depuis plusieurs endroits du code. | |
- | ==== Les déplacements ==== | + | |
- | + | ||
- | déplacement = changement de l'adresse d'un objet, l'objet est conservé à l'identique. L'objet initial n'est plus valide. | + | |
- | + | ||
- | {{ :move.png |}} | + | |
- | + | ||
- | En pratique, un déplacement peut être une copie déguisée : copie puis suppression de l'objet initial (dans ce cas, le déplacement aura le même coût que la copie). Ça sera le cas des types fondamentaux. Pour des objets plus complexes (par exemple ''std::string'', ''std::vector'', etc), toutes les données ne seront pas copiée. (dans ce cas, le déplacement sera beaucoup moins couteux que la copie). | + | |
<code cpp> | <code cpp> | ||
- | void g(int j) {} | + | #include <iostream> |
- | void f() { | + | int f(int j) { |
- | int i { 123 }; // non const, le déplacement est une modification | + | return j + 456; |
- | g(std::move(i)); // l'objet contenu dans la variable i est déplacé dans | + | } |
- | // la variable j de la fonction g, i devient invalide | + | |
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | i = f(i); | ||
+ | std::cout << i << std::endl; | ||
} | } | ||
</code> | </code> | ||
- | Concerne aussi la sémantique de valeur. Mais indépendant de la copie (c'est-à-dire un objet peut être copiable et movable, copiable et non movable, ou non copiable et non movable) | + | affiche : |
+ | <code> | ||
+ | 579 | ||
+ | </code> | ||
- | ===== indirections ===== | + | Dans ce code, l'objet initialement dans la variable ''i'' est dans un premier temps copie dans la variable ''j''de la fonction ''f'', puis le resultat est deplacer depuis la variable ''j'' de la fonction ''f'' vers la variable ''i'' de la fonction ''main''. Cela fait beaucoup de manipulation d'objets. |
- | + | ||
- | Indirection = "objet" qui permet d'accéder à un autre objet à distance. Utiliser une indirection revient à utiliser l'objet qui est lié à l'indirection. | + | |
- | + | ||
- | {{ :indirect.png |}} | + | |
- | Si A est un objet et si B est une indirection sur A, alors utiliser B revient à utiliser A. | + | Les indirections sont un moyen d’accéder a un objet, sans devoir faire de copie ou de deplacement. Travailler sur une indirection revient a travailler sur l'objet indirectement. Toute modification sur l'indirection sera visible dans la variable d'origine et vice-versa. |
- | Exemple d'indirection déjà vu : les itérateurs. Un itérateur est une indirection sur un élément d'une collection. Accéder à l'itérateur revient à accéder à l'élément. | + | Le code precedent peut etre modifie de la facon suivante : |
<code cpp> | <code cpp> | ||
- | #include <iostream> | + | void f(int & j) { // notez bien l'ajout de & ici |
- | #include <vector> | + | j += 456; |
+ | } | ||
int main() { | int main() { | ||
- | std::vector<int> v { 1, 2, 3, 4 }; | + | int i { 123 }; |
- | auto it = std::begin(v); | + | f(i); |
- | std::cout << (*it) << std::endl; | + | std::cout << i << std::endl; |
- | (*it) = 5; | + | |
- | for (auto i: v) { std::cout << i << ' '; } | + | |
- | std::cout << std::endl; | + | |
} | } | ||
</code> | </code> | ||
Ligne 134: | Ligne 153: | ||
<code> | <code> | ||
- | 1 | + | 579 |
- | 5 2 3 4 | + | |
</code> | </code> | ||
- | ''it'' permet d'accéder au premier élément du tableau. | + | Dans ce code, la variable ''j'' dans la fonction ''f'' est une indirection (une reference) vers la variable ''i'' de la fonction ''main''. Utiliser ''j'' revient a utiliser indirectement ''i'', la modification realisee sur ''j'' est en fait realisee sur ''i''. |
- | ==== Validité d'une indirection ==== | + | {{ :copie-move-3.png |}} |
- | Le problème de la validité d'une indirection a déjà été abordé pour les itérateurs. Pour rappel : | + | Il n'y a pas de copie ou de deplacement dans ce code. Les indirections seront donc souvent utilisees pour eviter la copie d'objets complexes ou pour modifier un objet dans une fonction. Lorsque la fonction ne doit pas modifier l'objet, la reference sera mis en constante avec le mot-cle ''const''. |
<code cpp> | <code cpp> | ||
- | #include <iostream> | + | void f(int & i) { |
- | #include <vector> | + | i += 456; // ok, la reference n'est pas constante |
+ | } | ||
- | int main() { | + | void g(int const& i) { |
- | std::vector<int> v { 1, 2, 3, 4 }; | + | i += 456; // erreur, la reference est constante |
- | auto it = std::begin(v); | + | |
- | std::cout << (*it) << std::endl; | + | |
- | v.clear(); | + | |
- | std::cout << (*it) << std::endl; | + | |
} | } | ||
</code> | </code> | ||
- | Après l'appel à ''clear'', l'élément correspondant à l'itérateur n'est plus valide et utiliser ''it'' produite donc un comportement indéterminé. | + | affiche l'erreur suivante : |
- | {{ :reference2.png |}} | + | <code> |
+ | main.cpp:6:7: error: cannot assign to variable 'i' with const-qualified type 'const int &' | ||
+ | i += 456; | ||
+ | ~ ^ | ||
+ | main.cpp:5:19: note: variable 'i' declared const here | ||
+ | void g(int const& i) { | ||
+ | ~~~~~~~~~~~^ | ||
+ | </code> | ||
- | Et retenez surtout qu'un “undefined behavior” ne produit pas forcément un crash (c'est le cas de ce code, il semblera valide à l'exécution, mais pourra faire n'importe quoi) et ne produira JAMAIS d'erreur de compilation. | + | Pour pouvez egalement trouver les syntaxes equivalentes suivantes pour ecrire une reference constante : |
- | Ce qui est valide pour l'itérateur est valide pour toutes les indirections : il faut que l'objet correspond à l'indirection soit valide, sinon l'indirection est invalide et l'utiliser produit un “undefined behavior”. | + | <code cpp> |
+ | TYPE const & NOM | ||
+ | const TYPE & NOM | ||
+ | </code> | ||
+ | N'oubliez pas aussi que l'utilisation des espaces est libre en C++, vous pouvez donc trouver ces syntaxes avec ou sans espace avant et apres la reference ''&''. | ||
- | ==== Types d'indirection ==== | + | La regle est que le mot-cle ''const'' s'applique au type qui se trouve a gauche. Et s'il n'y a rien a gauche, il s'applique a droite. |
- | classification : | + | Pour comprendre une syntaxe avec reference, il est souvent plus simple de lire de la droite vers la gauche. Ainsi, la ligne ''TYPE const & NOM'' peut se lire : "NOM est une reference sur une constante de type TYPE". |
- | * peut être null ou non (la référence doit toujours être valide) | + | |
- | * managé ou non = responsable de détruire l'objet (référence = non managé) | + | |
- | * affectable ou non (référence = non affectable) | + | |
- | * cas particulier des itérateurs (indirection sur élément d'une collection) | + | |
- | Il n'y a pas besoin de voir en détail toutes les indirections, certaines sont moins utilisées et seront vues dans les chapitres complémentaires. | ||
- | * référence (lvalue et rvalue ?) | + | ==== Classification des indirections ==== |
- | * ''std::reference_wrapper'' (affectable) | + | |
- | * itérateur = indirection sur collection | + | |
- | * pointeur = peut être null | + | |
- | * pointeur intelligent = managé (partagé = ''std::shared_ptr'' ou non = ''unique_ptr''), pointeur nu = non managé | + | |
+ | Les references vues precedent ne sont qu'un des nombreux types d'indirections qui existent. C'est le type d'indirection qui est le plus utilise et celui que vous devrez utiliser par defaut. | ||
- | ==== Note sur les indirections en C et C++ ==== | + | Il est meme possible de creer des classes qui representent une indirection, il y a donc potentiellement un nombre infini de types differents d'indirections. Le but de ce chapitre n'est donc pas de presenter toutes les indirections, mais uniquement de donner les caracteristiques des indirections du langage C++ et de la bibliotheque standard. |
- | Même si le C et le C++ sont deux langages différents, ils partagent une origine commune (d'où leur noms qui sont proches et des syntaxes que l'on peut retrouver dans les deux langages) et surtout, il est courant d'utiliser des bibliothèques écrites en C dans un programme C++. | + | === Semantiques de reference et de pointeurs === |
- | Malgré cette origine commune, ces deux langages ont des mécanismes très différents, en particulier concernant la gestion de la mémoire. Mais dans ce chapitre, c'est un autre point qui va être abordé : les indirections. | + | Pour commencer, il y a deux types principaux d'indirections : les indirections a semantiques de reference et les indirection a semantiques de pointeurs. |
- | Le langage C propose qu'un seul type d'indirection, les pointeurs null, alors que le C++ en proposent différents types (voire virtuellement une infinité, puisqu'il est possible de définir des classes implémentant des indirections, comme c'est le cas des itérateurs par exemple). | + | <note>**Semantiques** |
- | Cela peut sembler être une difficulté du C++ par rapport au C, puisque cela nécessite d'apprendre plusieurs syntaxes et concepts, là où il n'en faut qu'un seul en C. En fait, chaque type d'indirection apporte des fonctionnalités et garanties différentes, ce qui peut simplifier le code. Il est donc important de bien comprendre les spécificités de chaque type d'indirections et de les utiliser correctement. | + | Une sémantique est le sens qui est donne a un concept, c'est a dire l'ensemble des operations que ce concept permet de faire. |
- | Au contraire, en C, les pointeurs null seront utilisés comme indirection sur un objet, comme indirection sur un élément d'un tableau ou pourront même représenter un tableau. Il n'est pas possible de savoir ce que l'on peut faire ou non avec un pointeur uniquement en regardant son type (idem pour le compilateur, ce qui veut dire qu'il ne peut rien vérifier), il faut regarder le contexte d'utilisation du pointeur pour savoir comment l'utiliser. | + | Une "indirection a semantique de reference" est donc une indirection qui n'est pas forcement une reference, mais qui se manipulera comme une reference. Et de meme, une "indirection a semantique de pointeur" est une indirection qui se manipule comme un pointeur, sans forcement etre un pointeur. (Les pointeurs seront vu dans la partie sur la programmation orientée objet). |
- | Un dernier point sur les indirections : il est courant de trouver dans des codes C++ (sur internet ou dans de vrais projets) qui sont en fait des codes C (généralement du C++ écrit en utilisant les pratiques du C). Dans ces codes, vous verrez régulièrement des pointeurs null, puisque c'est le seul type d'indirection disponible en C. Cependant, en C++, les pointeurs null sont le type d'indirection qui apportent le moins de garanties et leur utilisation doit donc être limité aux profits des autres types d'indirection (en premier lieu les références). | + | Par exemple, les itérateurs que vous avez etudie avec les collections sont des indirections a semantique de pointeur, mais qui ne sont pas des pointeurs. |
+ | </note> | ||
- | Faites bien attention d'identifier les codes C++ problématiques et corrigez les si nécessaire. | + | Les références sont des indirections qui permettent d'acceder directement a un objet. La syntaxe pour utiliser l'indirection est exactement la meme que la syntaxe pour utiliser l'objet. Dans certains cas, le compilateur peut même remplacer l'indirection par un //alias de variable//, c'est a dire utiliser directement la variable référencée, mais avec un nom different. (Le code généré par le compilateur supprimera complètement l'indirection). |
+ | <code cpp> | ||
+ | int i { 123 }; | ||
+ | int & j { i }; | ||
- | ===== Fonctions ===== | + | j += 456; // strictement equivalent a i += 456; |
+ | </code> | ||
- | Pourquoi présenter les indirections dans la partie sur les fonctions ? Les indirections sont utilisables en dehors des fonctions (en particulier les itérateurs), mais elles sont particulièrement intéressantes avec les fonctions. | + | Les pointeurs sont des indirections qui ne permettent pas d'utiliser directement un objet. Pour accéder a l'objet, il faut d'abord appliquer une opération particulière, appellee "déréférencement". "Déréférencer un pointeur" consiste donc a accéder a l'objet pointe par un pointeur. |
- | On va se limiter à un type d'indirection : les références. | + | Par exemple, pour utiliser un iterateur, vous avez vu dans le chapitre [[iterateurs|]] qu'il fallait utiliser l'opérateur ''*'', place devant la variable. |
- | ==== Pile d'appel de fonction ==== | + | <code cpp main.cpp> |
+ | #include <iostream> | ||
+ | #include <vector> | ||
+ | |||
+ | int main() { | ||
+ | const std::vector<int> v { 12, 23, 34 }; | ||
+ | const auto it = cbegin(v); | ||
+ | std::cout << (*it) << std::endl; | ||
+ | } | ||
+ | </code> | ||
- | Explications simplifiées pour comprendre la mécanisme. //Stack// en anglais. | + | affiche : |
- | Lorsqu'une fonction est appelée (cas particuler pour la fonction main qui est appelée par le système), cela créé en mémoire un contexte pour cette fonction. Ce contexte contient les informations nécessaires pour l'exécution de la fonction, en particulier la mémoire correspondant aux variables locales de la fonction (ainsi que les paramètres d'appel, que l'on peut considérer comme des variables locales). | + | <code> |
+ | 12 | ||
+ | </code> | ||
+ | |||
+ | Dans ce code, ''it'' est un itérateur (une indirection) sur le premier élément de la collection et ''*it'' (déréférencement de l'itérateur) correspond a ce premier élément (la valeur entière 12). | ||
+ | |||
+ | <note> | ||
+ | Lorsque le nom d'une variable n'a pas d'importance (par exemple dans un code d'exemple ou pour des explications), il est classique de nommer une référence par ''ref'' et un pointeur par ''ptr'', ''p'' ou ''q''. | ||
+ | </note> | ||
<code cpp> | <code cpp> | ||
- | void f(int i) { | + | // indirection a sémantique de référence |
- | int j {}; | + | ref = 123; |
- | int k {}; | + | std::cout << ref << std::endl; |
- | } | + | |
+ | // indirection a sémantique de pointeur | ||
+ | (*ptr) = 123; | ||
+ | std::cout << (*it) << std::endl; | ||
</code> | </code> | ||
- | {{ :pile.png |}} | + | Les parenthèses ne sont pas forcément indispensables, l’opérateur de déréférencement ''*'' est prioritaire par rapport a l’opérateur d'affectation ''='' et l’opérateur de flux ''<<''. C'est a dire que le code ''*ptr = 123;'' sera interprété comme équivalent a ''(*ptr) = 123;'' (le pointeur est déréférencé PUIS une valeur est affectée a l'objet pointé) et non comme ''*(ptr = 123);'' (le pointeur est modifié PUIS il est déréférencé). |
- | A chaque fois qu'une fonction est appelée, le nouveau contexte "s'empile" sur les précédents (on parle de "Pile d'appel des fonctions"). | + | La dernière syntaxe est valide, mais produira très probablement un crash. Manipuler les pointeurs de cette façon s'appelle l’arithmétique des pointeurs, c'est quelque chose de très complexe et ne donc être réalisé que dans des cas très particuliers (interfaçage avec le système, le matériel ou d'autres langages). |
- | <code cpp> | + | Dans ce cours, pour éviter les ambiguïtés sur l'ordre d’évaluation des opérateurs ou la confusion avec l’opérateur de multiplication ''*'', les parenthèses seront toujours utilisées. |
- | void h() { | + | |
- | double x {}; | + | |
- | double y {}; | + | |
- | double z {}; | + | |
- | } | + | |
- | void g() { | + | ==== Validité des indirections ==== |
- | bool a {}; | + | |
- | bool b {}; | + | |
- | bool c {}; | + | |
- | h(); | + | |
- | } | + | |
- | void f() { | + | Vous avez vu dans le chapitre sur les itérateurs que ceux-ci pouvait être invalide. Quand c’est le cas, ils prennent une valeur particulière, correspondant a ''std::end()'', il faut donc toujours tester un itérateur avant utilisation. |
- | int i {}; | + | |
- | int j {}; | + | <code cpp> |
- | int k {}; | + | assert(it != std::end(v)); |
- | g(); | + | (*it) = 123; |
- | } | + | |
</code> | </code> | ||
- | {{ :pile.png |}} | + | Vous avez également vu qu'un itérateur pouvait être invalide, mais ne pas être testable, par exemple après que la collection soit modifiée. |
- | Lorsque f est exécutée, la pile contient le contexte d'exécution de f. Après l'appel de g, la pile contient f et g, puis après l'appel de h, la pile contient f, g et h. Après le retour de h, la pile contient à nouveau f et g, et après le retour de g, la pile contient f. | + | <code cpp> |
+ | std::vector<int> v { 1, 2, 3 }; | ||
+ | auto it = std::begin(v); | ||
+ | v.clear(); | ||
+ | assert(it != std::end(v)); | ||
+ | (*it) = 123; // erreur | ||
+ | </code> | ||
- | Note : mémoire = très gros tableau d'octets. l'"empilement" est conceptuel, il n'y a pas de notion de "dessus" et "dessous" dans une mémoire. | + | Certaines opérations peuvent invalider les itérateurs, et seule la documentation permet de savoir cela. En cas de doute, considérez que les itérateurs sont invalides. |
- | **Contexte d'une fonction** | + | Mais en fait, ce problème de validité n'est pas spécifique aux itérateurs, mais a toutes les indirections. Certains types d'indirection (comme les références ou les pointeurs intelligents) permettent d'avoir des garanties plus fortes si elles sont correctement utilisées, mais il faut toujours rester vigilants. |
- | Le contexte d'une fonction est l'ensemble des variables qu'une fonction peut utiliser. | + | L’équivalent de ''std::end()'' pour les pointeurs est ''nullptr'' ("pointeur nul"), il est donc possible de tester un pointeur avec ''assert''. Mais, comme pour les itérateurs, un pointeur peut être invalide, mais ne pas être testable ("dangling pointer"). |
<code cpp> | <code cpp> | ||
- | void g() { | + | assert(ptr != nullptr); |
- | i = 123; // erreur | + | assert(ptr); // équivalent |
- | } | + | </code> |
- | void f() { | + | Apres avoir déréférencé une indirection a sémantique de pointeur, vous obtenez encore une indirection, mais a sémantique de référence cette fois ci. Cette nouvelle indirection peut être conservée dans une variable, comme n'importe quelle indirection. |
- | int i {}; | + | |
- | g(); | + | <code cpp> |
- | } | + | std::vector<int> v { 1, 2, 3 }; |
+ | auto it = std::begin(v); | ||
+ | assert(it != std::end(v)); | ||
+ | int & ref = (*it); | ||
+ | ref = 123; // ok, equivalent a (*it) = 123 | ||
</code> | </code> | ||
- | Exception : | + | Mais vous voyez dans ce code qu'il est facile d'invalider une référence. Si vous appelez ''clear'' après avoir créé la référence, celle-ci sera invalide, tout comme l'itérateur. |
- | * variables globales, accessibles partout. A éviter (complexifie la lecture du code) | + | |
- | * les littérales (il faut bien qu'elles soient quelque part dans la mémoire. Elles sont dans un espace dédié, le data segment. Et il y a un segment spécifique pour le texte. Mais tout cela est géré en interne par le compilateur, pas besoin de s'en préoccuper). | + | |
- | **Taille limite de la Pile** | + | Dans ces conditions, pourquoi une référence est plus sûre qu'un pointeur ? La raison est qu'elle n'est pas destinée a être utilisée dans les mêmes conditions qu'un pointeur. |
- | Il existe une limite à la taille de la pile (par exemple 1 Mo avec MSVC). Cela dépend des configurations et du système. Mais si vous essayez de créer un milliard de ''int'' dans une fonction, cela va produire très certainement un dépassement de pile. (erreur de type "Segmentation fault"). | + | * un pointeur est modifiable, il peut recevoir une nouvelle valeur (affectation), être nul, faire des opérateurs arithmétiques dessus. Une référence est définie a l'initialisation et ne sera plus modifiable ensuite. |
+ | * un pointeur pourra être transmis entre differentes fonctions et classes et avoir une duree de vie assez longue. Une reference ne sera pas conservee lorsque la duree de vie des objets n'est pas garantie. | ||
- | 2 conséquences : | + | La difference de securite entre pointeur et reference tient donc uniquement a leur utilisation. Les references imposent des contraintes plus fortes, mais offrent en retour des garanties plus fortes. Pour ameliorer la securite du code, il faut donc priviliger l'utilisateur des references, c'est a dire de concevoir au maximum son code pour rester dans les contraintes imposees par les references. (C'est pour cette raison que les pointeurs sont vu tres tardivement dans ce cours). |
- | Appeler une fonction consomme un peu de mémoire sur la pile (même si la fonction n'a pas de variable locale). Appelle récursif illimité finira donc pas saturer la pile et produire un crash. | + | <note>Cette approche est assez classique en C++. Pour résoudre un probleme, il est souvent possible d'utiliser plusieurs approches : des approches plus génériques (moins de contraintes, mais également moins de garanties) ou des approches plus spécialisées (plus de contraintes, donc plus de garanties). |
- | Parfois (souvent) on a besoin d'utiliser plus que 1 Mo de mémoire (penser à une simple image en mémoire, cela peut prendre plusieurs Mo). Il existe un second espace mémoire, le Tas (ou mémoire dynamique), qui contient tout ce qui n'est pas sur la Pile. | + | En programmation moderne, la taille des projets est de plus en plus grande (ainsi que la complexité), la qualité du code est donc de plus en plus prioritaire. C'est pour cela que les langages modernes ont des bibliothèques standards de plus en plus importantes et le C++ n’échappe pas a cette règle : le langage C ne propose qu'un seul type d'indirection (les pointeurs), alors que le C++ en propose beaucoup plus (pointeurs du C, références, itérateurs, pointeurs intelligents, etc.). |
- | Différences Pile/Tas | + | Cela pourrait laisser penser que les langages modernes sont plus complexes et plus long a apprendre (la norme C tient en 400 pages, la norme C++ en 1500 pages et la documentation du Java doit faire plusieurs milliers de pages), mais ça serait une vision fausse. Si vous souhaitez réaliser une tâche en particulier, vous aurez dans le premier cas peu de fonctionnalités utilisables (donc facile a apprendre), mais vous devrez tout faire vous même, et donc avoir un code plus important et plus complexe (et plus de risque d'erreur). Dans le second cas, vous aurez plus d'outils a apprendre, mais ils seront plus puissants pour chaque tâche. Le code sera donc plus simple et avec moins d'erreur. |
- | * Pile = taille très limité, Tas = beaucoup plus de mémoire (en première approximation = infini... en vrai = taille limité, espace libre généré par le système et peut changer avec le temps = besoin de gérer si la mémoire est dispo ou non) | + | |
- | * Pile = généré automatiquement lors des appels et retour de fonction, Tas = à gérer par le code | + | |
- | Cas particulier des objets complexes comme ''std::vector'' et ''std::string''. Ils sont en fait constitués de plusieurs parties : | + | Et en C++, vous devrez étendre cette approche a votre code. Si un outil n'est pas disponible dans le langage ou une bibliothèque, plutôt que d’écrire directement le code pour résoudre cette tâche, il faudra faire en sorte de créer ces outils, de les rendre réutilisable et sécurisé, puis de les utiliser pour résoudre votre problème. |
- | * une petite partie, de taille fixe, qui sera sur la Pile par défaut | + | </note> |
- | * les données proprement dites (les éléments ou le texte), qui sont sur le Tas | + | |
- | * des indirections depuis la partie sur la Pile vers la Partie sur le Tas | + | |
- | __image d'un vector__ | ||
- | Des collections plus complexes (''std::list'' par exemple) peuvent contenir plusieurs objets différents en mémoire, liés entre eux par des indirections. | + | ==== Les références comme parametre de fonction ==== |
- | En pratique, il n'y a pas de limite au nombre de sous-objets que peut contenir un objet complexe, et il peut y avoir des organisations très complexe en mémoire. | + | Quelles sont les conditions pour que les références restent valides lors des appels de fonctions ? |
- | Note sur ''std::array'' : toutes les données sont sur la Pile, il y aura donc un problème si tableau est trop grand. Il est préférable d'utiliser ''std::vector'' (ou dynarray) si c'est le cas. | + | Le cas le plus simple est lorsque la référence est utilisée comme paramètre de fonction. |
- | Remarque sur vector et copie/déplacement. Quand on copie, cela copie toutes les données. Quand on move, cela copie simplement la petite partie sur la Pile : c'est plus performant. (Mais le plus performant reste d'utiliser une indirection). | + | <code cpp main.cpp> |
+ | #include <iostream> | ||
- | <note>Déplacer une partie des explications dans un chapitre complémentaire</note> | + | void f(int & ref) { |
+ | ref = 456; | ||
+ | } | ||
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | f(i); | ||
+ | std::cout << i << std::endl; | ||
+ | } | ||
+ | </code> | ||
- | ==== Passage de valeurs ==== | + | affiche : |
- | Contextes de fonctions indépendants = pas possible d'accéder directement à une variable. Comment faire ? | + | <code> |
+ | 456 | ||
+ | </code> | ||
- | <code cpp> | + | Dans ce cas, vous avez la garantie que la fonction appelée ''f'' se terminera avant la fonction appelante ''main''. L'objet provenant de ''main'' sera donc forcément valide pendant toute la duree de l'exécution de la fonction ''f'' et il n'y a aucun risque d'avoir une référence invalide. Et cela serait encore valide si la fonction ''f'' appelait une autre fonction, puis une autre fonction et ainsi de suite. |
- | void g() { | + | |
- | i = 123; // erreur | + | Pour le retour de fonction, la situation est différente. |
+ | |||
+ | <code cpp main.cpp> | ||
+ | #include <iostream> | ||
+ | |||
+ | int & f() { | ||
+ | int i { 123 }; | ||
+ | return i; | ||
} | } | ||
- | void f() { | + | int main() { |
- | int i {}; | + | int & i { f() }; // probleme |
- | g(); | + | std::cout << i << std::endl; |
} | } | ||
</code> | </code> | ||
- | On copie l'objet depuis le contexte de f dans le contexte de g. Il est alors possible d'utiliser cet objet dans g. | + | Dans ce code, la fonction ''f'' retourne une référence sur une variable locale a la fonction. Lorsque la fonction se termine (c'est a dire tout de suite après que la référence soit créée, puisque la référence est le paramètre de retour), la variable locale ''i'' est détruite et la référence devient invalide. |
- | La syntaxe est celle déjà vue : | + | **Ne JAMAIS retourner une référence sur une variable locale !** |
- | <code> | + | Notez bien que ce code ne produit pas d'erreur (et affichera peut être la valeur correcte "123"). Une référence étant considérée comme toujours valide, il n'y a pas de vérification effectuée. Cela va produire un comportement indéterminée (//undefined behavior//), c'est de la responsabilité du développeur de vérifier son utilisation des références. |
- | // déclaration | + | |
- | void f(TYPE NOM) | + | |
- | // appel | + | Une référence ne peut être retournée par une fonction uniquement si elle correspond a un objet dont la durée de vie n'est pas limité par la fonction. Par exemple une référence qui serait passé en paramètre. |
- | f(VALEUR) // littérale, expression, variable | + | |
+ | <code cpp> | ||
+ | int & f(int & ref) { | ||
+ | ref += 123; | ||
+ | return ref; //ok | ||
+ | } | ||
</code> | </code> | ||
- | Par exemple : | + | Dans tous les cas, faites bien attention quand vos objets sont créées et détruits et quand une indirection est valide ou non. |
+ | |||
+ | |||
+ | ==== Rvalue-reference ==== | ||
+ | |||
+ | Depuis le début de ce cours, le terme de "valeur" (//value//) est utilisé indifféremment pour désigner une littérale, une variable (constante ou non) ou une expression (un calcul, un retour de fonction, etc). | ||
+ | |||
+ | En fait, il existe différents types de valeurs, qui respectent des règles sémantiques relativement complexes. Il n'est pas nécessaire de toutes les connaître dans un premier temps, seules deux grandes catégories sont intéressantes au début. (Et les explications vont être simplifiées). | ||
+ | |||
+ | * les //lvalues// (//left-value//) : se sont les variables (constante ou non) ; | ||
+ | * les //rvalues// (//right-value//) : ce sont tout le reste, c'est a dire les valeurs temporaires (les littérales ou les expressions). | ||
+ | |||
+ | Cette distinction va être importante pour le passages de valeurs dans les fonctions. Sans référence, c'est a dire lors d'un passage par valeur (copie), vous avez vu qu'il est possible d'appeler une fonction avec n'importe quel type de valeur. | ||
<code cpp> | <code cpp> | ||
- | void g(int i) { | + | void f(int) {} |
- | i = 123; // ok | + | |
- | } | + | |
- | void f() { | + | int main() { |
- | int i {}; | + | int i { 123 }; |
- | g(i); | + | f(i); // ok avec une variable (lvalue) |
+ | f(123); // ok avec une littérale (rvalue) | ||
+ | f(12+34) // ok avec une expression (rvalue) | ||
} | } | ||
</code> | </code> | ||
- | L'objet est perdu à la fin de la fonction (sauf si retourné), donc les modifications faites sont perdues. | + | Avec les références constantes, la situation est identique, vous pouvez passer n'importe quel type de valeur. |
<code cpp> | <code cpp> | ||
- | void g(int i) { | + | void f(int const&) {} |
- | i = 123; // ok | + | |
- | } | + | |
- | void f() { | + | int main() { |
- | int i {}; | + | int i { 123 }; |
- | g(i); | + | f(i); // ok avec une variable (lvalue) |
- | std::cout << i << std::endl; // affiche 0 | + | f(123); // ok avec une littérale (rvalue) |
+ | f(12+34) // ok avec une expression (rvalue) | ||
} | } | ||
</code> | </code> | ||
- | différence entre copie et déplacement ? (selon le type de value, rvalue ou lvalue, ''std::move'') | + | La situation change pour les références non constante. Celle-ci ne peuvent accepter qu'un seul type de valeur : les //lvalue//. |
- | retour de valeur | + | <code cpp> |
+ | void f(int &) {} | ||
+ | int main() { | ||
+ | int i { 123 }; | ||
+ | f(i); // ok avec une variable (lvalue) | ||
+ | f(123); // erreur | ||
+ | f(12+34) // erreur | ||
+ | } | ||
+ | </code> | ||
- | ==== Passage par référence ==== | + | D'ailleurs, ce type de référence est parfois appelle //lvalue-reference//, pour indiquer qu'elles n'acceptent que des //lvalue//. |
- | Indirection sur l'objet de ''f'' dans ''g''. Chaque indirection a une syntaxe spécifique. Pour la référence, 2 types : const ou non. | + | Il existe en fait un second type de référence, qui n'acceptent que des //rvalue// et qui s'appelle donc //rvalue-reference//. (Lorsque le type de référence n'est pas précisé, il s'agit de //lvalue-reference//). Les //rvalue-reference// s’écrivent avec l’opérateur ''&&'' et ne sont jamais constantes. |
- | <code> | + | <code cpp> |
- | // déclaration | + | void f(int &&) {} |
- | void f(TYPE const& NOM) // const | + | |
- | void f(TYPE & NOM) // non const | + | |
- | // appel | + | int main() { |
- | f(VALEUR) // littérale, expression, variable | + | int i { 123 }; |
+ | f(i); // erreur | ||
+ | f(123); // ok avec une littérale (rvalue) | ||
+ | f(12+34) // ok avec une littérale (rvalue) | ||
+ | } | ||
</code> | </code> | ||
- | const = on ne peut pas modifier le paramètre, non const = on peut. | + | Pour résumer : |
- | Dans tous les cas, ce n'est pas la référence que l'on manipule, mais c'est en fait l'objet dans ''f''. En particulier, si on fait une modification dans ''g'', elle sera visible dans ''f''. | + | ^ Passage ^ lvalue ^ rvalue ^ |
+ | | Par valeur | oui | oui | | ||
+ | | Référence constante | oui | oui | | ||
+ | | Référence | oui | **non** | | ||
+ | | Rvalue-reference | **non** | oui | | ||
+ | |||
+ | <note>**Pourquoi deux types de référence ?** | ||
+ | |||
+ | Les //lvalue// et les //rvalue// ont un comportement très différents lorsqu'elles sont utilisées dans une fonction. La première continuera d'exister après l'appel de la fonction, il est donc possible de modifier la variable dans la fonction. | ||
<code cpp> | <code cpp> | ||
- | void g(int & i) { // on modifie i donc non const | + | void f(int & i) { |
- | i = 123; // ok | + | i += 12; |
} | } | ||
- | void f() { | + | int main() { |
- | int i {}; | + | int i { 123 }; |
- | g(i); | + | f(i); |
- | std::cout << i << std::endl; // affiche 123 ! | + | std::cout << i << std::endl; |
} | } | ||
</code> | </code> | ||
- | Note : une indirection prend un peu de place dans la Pile. Environ le même cout qu'un type fondamental (int, float, etc) | + | Au contraire, une //rvalue// (le résultat d'un calcul ou la valeur retournée par une fonction) existe que temporairement, elle n'est donc plus accessible après l'appel de la fonction. L'utilisation d'une //rvalue-reference// permet de dire au compilateur : "cette valeur ne sera plus utilisée dans la fonction appelante par la suite, tu peux donc optimiser comme tu veux l'utilisation de cette valeur". |
+ | C'est donc avant tout une question d'optimisation, qui sera surtout intéressant pour les classes complexes. Cela sera vu plus en détail dans la partie programmation orientée objet. Le plus souvent, vous pourrez utiliser les références (//lvalue//) non constante lorsque vous voulez modifier une variable dans une fonction et une (//lvalue//) référence constante dans les autres cas (ou un passage par valeur lorsque la copie est peu coûteuse, par exemple pour les types fondamentaux ''int'', ''double'', etc.) | ||
+ | </note> | ||
- | Conclusion : que faut-il utiliser ? | + | Ces règles sont valides aussi pour les paramètres par défaut : |
- | * pour éviter la copie = indirection. Si la copie n'est pas couteuse (types fondamentaux) ou si on a besoin d'une copie (si on veut faire des modifications sans les conserver) | + | |
- | * pour modifier un objet = référence non const | + | |
- | * par défaut (si on ne sait pas si la copie est couteuse) = référence constante. | + | |
+ | <code cpp> | ||
+ | void f(int i = 0); // ok | ||
+ | void f(int const& i = 0) // ok | ||
+ | void f(int & i = 0); // erreur (littérale vers lvalue-reference) | ||
+ | </code> | ||
- | ==== Compatibilité entre paramètres et arguments ==== | + | <note>**std::move** |
- | * lvalue : variable... | + | Pour être plus précis sur le rôle de la fonction ''std::move'', celle-ci ne "déplace" pas les objets ou n'autorise pas le compilateur a faire un déplacement. Elle convertie simplement une valeur en //rvalue-reference//. Le compilateur va donc interpréter cette valeur comme si c’était une valeur temporaire et pourra appliquer les optimisations compatibles avec un temporaire (déplacement ou copie le cas échéant). |
- | * rvalue : temporaire (littérale, expression)... | + | </note> |
+ | |||
+ | Les differents types de références permet d'écrire des surcharges de fonctions. Cela sera vu dans le prochain chapitre. | ||
+ | |||
+ | |||
+ | ==== Copie et déplacement d'indirections ==== | ||
+ | |||
+ | Les indirections sont des types comme les autres et peuvent donc être utilisés comme n'importe quel type. Par exemple, il est possible de créer des variables (comme déjà vu) : | ||
- | * passage par valeur et référence constante : accepte lvalue et rvalue | ||
- | * passage par référence : accepte lvalue uniquement | ||
- | * passage par rvalue référence : accepte rvalue uniquement | ||
- | |||
<code cpp> | <code cpp> | ||
- | void f(int x); | + | int i { 123 }; |
- | voif g(int const& x); | + | int & j { i }; |
- | void h(int & x); | + | </code> |
- | void i(int && x); | + | |
- | int x {}; // lvalue | + | La seconde ligne de code peut se lire : ''j'' est une variable, de type "référence sur un entier", qui contient comme valeur une indirection sur la variable ''i''. |
- | f(x); // ok | + | |
- | g(x); // ok | + | |
- | h(x); // ok | + | |
- | k(x); // erreur | + | |
- | // rvalue | + | Une référence (obtenue directement ou après déréférencement d'un pointeur) peut se convertir implicitement en valeur, par copie. (Il faut donc que le type soit copiable). |
- | f(1); // ok | + | |
- | g(1); // ok | + | <code cpp> |
- | h(1); // erreur | + | int i { 123 }; |
- | k(1); // ok | + | int & j { i }; |
+ | int & k { j }; | ||
+ | int l { j }; | ||
</code> | </code> | ||
- | <note>rvalue, lvalue, xvalue, prvalue, glvalue...</note> | + | Dans ce code, la variable ''j'' est une référence sur un entier (la variable ''i'', qui contient la valeur 123). La variable ''k'' est également une référence sur la variable ''i''. (Comme une référence peut être vue comme un alias de variable, elle pourra être supprimée par le compilateur. Et celui-ci sait très bien que ''j'' est aussi une référence, il n'y a pas de raison que ''k'' passe par ''j'' pour référencer ''i''). |
- | ==== Valeur par défaut ==== | + | Note : une conséquence directe a cela est qu'il est possible d'avoir autant d'indirections que vous souhaitez sur un même objet, il n'y a pas de limitation. |
+ | |||
+ | Au contraire, la variable ''l'' n'est pas une référence, mais contient un entier. C'est donc une copie de la valeur contenu dans la variable ''i'', et comme toujours lorsqu'il y a une copie, les objets sont indépendants et la modification d'une des deux variables ne sera pas répercutée sur l'autre. | ||
- | Selon le type de passage. Idem que les arguments, nécessite correspondance. | ||
- | |||
<code cpp> | <code cpp> | ||
- | void f(int x = 123); // ok, rvaleur vers value | + | j++; |
- | voif g(int const& x = 123); // ok, rvalue vers ref const | + | std::cout << i << std::endl; |
- | void h(int & x = 123); // erreur | + | l++; |
- | void i(int && x); // ? | + | std::cout << i << std::endl; |
</code> | </code> | ||
+ | affiche : | ||
- | ==== Paramètres de retour ==== | + | <code> |
+ | 124 | ||
+ | 123 | ||
+ | </code> | ||
- | Ne pas retourner une indirection sur un variable locale, qui sera détruite à la fin de la fonction (donc indirection sur variable invalide). | + | A noter qu'un objet complexe peut tout a fait contenir des indirections en interne. Dans ce cas, la copie de cet objet (si elle est autorisée) peut produire différents comportement, selon comment cette classe est conçue. Par exemple, les classes ''std::string'' et ''std::shared_ptr'' (un type de pointeur intelligent) utilisent des pointeurs sur des objets internes. La copie d'un objet de type ''std::string'' copie l’intégralité du tableau interne et l'objet copié est indépendant du premier objet (//deep copy//, copie en profondeur). Au contraire, la copie d'un objet de type ''std::shared_ptr'' ne copie pas l'objet interne et la copie pointe sur le même objet que le pointeur original (//shallow copy//, copie superficielle). |
- | (N)RVO : optimisation pour retour, le plus efficace. + conditions d'utilisation. | + | |
+ | ==== Inférence de type ==== | ||
+ | |||
+ | Les indirections sont également utilisable avec l’inférence de type. Dans le chapitre sur l’inférence, vous avez vu une différence importante entre ''auto'' et ''decltype'' : le premier ne conserve pas les modificateurs de types, le second oui. Les références et la constance sont des exemples de modificateurs de type, il faudra donc faire attention a ajouter explicitement une référence si vous utilisez ''auto'' si nécessaire. | ||
<code cpp> | <code cpp> | ||
- | int g() { | + | int i { 123 }; |
- | int i; | + | int & ref { i }; |
- | return i; | + | |
- | } | + | auto a { ref }; // auto sera déduit en int |
+ | decltype(auto) b { ref }; // decltype(auto) sera déduit en int& | ||
+ | decltype(ref) c { i }; // decltype(ref) sera déduit en int& | ||
+ | |||
+ | auto & d { ref }; // auto sera déduit en int, donc le type sera int & | ||
</code> | </code> | ||
- | Par valeur, mais pas de copie ici, optimisé. Note : utilisation de déplacement = désactive optimisation, moins performant. | + | Cela est également applicable aux types de retour de fonction. |
- | + cas particulier ds fonctions pure | + | Il peut être perturbant d'avoir plusieurs mot-clés différents pour l’inférence de type, mais cela permet d'avoir plus de liberté dans les codes. Il faut retenir : |
+ | |||
+ | * ''auto'' lorsque vous souhaitez explicitement une valeur (par copie) ; | ||
+ | * ''auto &'' (constante ou non) lorsque vous souhaites explicitement une référence ; | ||
+ | * ''decltype'' lorsque vous souhaitez le type exacte | ||
<code cpp> | <code cpp> | ||
- | int g(int i) { | + | int f(); |
- | ... | + | int & g(); |
- | return i; | + | |
- | } | + | |
- | // alternative à | + | // force une valeur |
+ | auto value1 = f(); | ||
+ | auto value2 = g(); | ||
- | void g(int & i) { | + | // force une référence |
- | ... | + | auto & ref1 = f(); |
- | } | + | auto & ref2 = g(); |
+ | |||
+ | // ne force pas | ||
+ | decltype(auto) rv1 = f(); // valeur | ||
+ | decltype(auto) rv2 = g(); // référence | ||
</code> | </code> | ||
+ | |||
^ [[parametres_arguments|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[surcharge_fonctions|Chapitre suivant]] ^ | ^ [[parametres_arguments|Chapitre précédent]] ^ [[programmez_avec_le_langage_c|Sommaire principal]] ^ [[surcharge_fonctions|Chapitre suivant]] ^ | ||
+ |