Tu le fais systematiquement?
Ca ne rend pas ton code indigeste?
Version imprimable
Avec l'age j'ai un petit pool de fonction/classe qui gère ma mémoire de manière sécurisée dont le catch + rethrow est centralisé.
Ensuite:
ASSERT( ptr && "NULL pointer passed as argument");
ne prend pas 16567 lignes non plus.
Ce que tu dois comprendre, c'est qu'un pointeur est un type de variable particulier en cela qu'il contient... l'adresse mémoire à laquelle se trouve l'objet pointé, alors qu'une référence n'est qu'un alias, un autre nom, pour la variable d'origine.
De ce fait, je vais dire que tu t'en fous royalement du fait que tu travaille ou non avec une référence: cela s'applique, au final, exactement sur le même objet.
Il n'y a strictement aucune raison de faire une différence entre une référence et l'objet d'origine, simplement, parce que c'est la même chose ;)
cf plus hautCitation:
En effet, c'est plus léger syntaxiquement. C'est un avantage indéniable des références. Et pourtant, reste que c'est une duplication de fonctionalité et que cela introduit une ambiguïté syntaxique (unique au C++ d'ailleurs).
Ce que tu ne comprend pas, c'est qu'une référence remplace, effectivement, avantageusement les pointeurs, entre autres, quand l'objet référencé n'a strictement aucun sens à être nul.Citation:
Je suis au courant de ce que tu mentionnes au sujet des constructeurs, de new, de this, etc. Je ne faisais qu'illustrer que si les références étaient réellement sensées remplacer les pointeurs "dans la mesure du possible", elles auraient été implémentées différemment. "this" aurait été une référence, "new" aurait retourné une référence, la librairie standard C++ n'utiliserait que des références, la librairie standard C serait entièrement réimplémentée en termes de références, etc.
Tu ne peux absolument pas donner une telle garantie avec un pointeur, alors qu'avec une référence, elle est implicite et non contournable.
De plus, new (et delete) intervienne "par nature" dans la gestion dynamique de la mémoire, or, une référence n'a strictement aucun rapport avec celle-ci.
Sans oublier le fait que les objet créés sans avoir recour à l'allocation dynamique sont détruits automatiquement lorsque l'on sort de la portée dans laquelle ils sont déclarés.
On ne peut donc pas faire en sorte que new renvoie une référence, simplement parce qu'il renverrait... une référence sur un objet invalidé du fait de sa destruction automatique.
Enfin, si C++ est à considérer comme un langage totalement séparé de C, il revendique néanmoins son héritage par rapport au C.
Si on avait du réimplémenté la bibliothèque C standard pour qu'elle manipule des références, il y aurait eu d'une part beaucoup plus d'incompatibilités entre C et C++ et, d'autre part, on aurait du réinventé complètement la roue :aie:
C'est pourtant loin d'être les seuls cas où les références sont utilisées.Citation:
Vu que 1) la seule utilisation obligatoire des références dans le langage est la définition des surcharges d'opérateurs et des constructeurs de copie, et
Regarde l'ensemble de la STL, et tu remarquera qu'elles sont utilisées quasiment PARTOUT, que ce soit sur des fonctions libres ou sur des fonctions membres de classes ;)
Tu ne peux, effectivement, pas faire en sorte qu'une référence fasse référence à un autre objet, mais n'oublie pas que l'opérateur d'affectation ( = ) renvoie... une référence sur l'objet d'origine modifié :aie:Citation:
2) les contraintes d'utilisations les rendent inutilisables dans bien des cas (pas de réassignation et pas de nullité), ça m'apparaît plutôt un ajout tardif et sans grande importance. Je suis plutôt d'avis que les pointeurs restent la façon standard de "référencer".
Et la non nullité des références, et, justement, une sécurité que tu ne peux pas obtenir avec un pointeur.
Comme je dit précédemment, c'est un apport non négligeable parce que tu n'as, justement, pas besoin de tester ton pointeur.
Cela permet d'un coté d'éviter que les "incompétents" oublient de tester la non nullité du pointeur, et, pour les autres, d'éviter des tests inutiles, et donc des pertes de performances dans certaines situations.
Que demander de plus :question:
Tu semble oublier allègrement les opérateur de flux << et >>, qui travaillent sur les référence, et l'ensemble des autres classes (conteneurs en tête) qui manipulent essentiellement des références :aie:Citation:
Il ne faut pas chercher bien loin pourtant. Par exemple, les streams standards utilisent les pointeurs pour les fonctions read et write. Pas moyen de passer une string& ou quoique ce soit d'autre.
istream& read ( char* s, streamsize n );
ostream& write ( const char* s , streamsize n );
Les itérateurs ont, effectivement, sémantique de pointeur, simplement parce qu'il faut bien faire la différence entre ce qui est de la responsabilité de l'itérateur (la capacité à passer à l'élément suivant) et ce qui est de la responsabilité de l'objet itéré.Citation:
Ou encore, la STL est composée de trois choses: des conteneurs, des algorithmes et des itérateurs. Or les itérateurs sont implémentées comme des pointeurs: ils peuvent être déréférencés, il peuvent être incrémentés, et ils peuvent pointer sur "rien" (sémantiquement).
Comme, de plus, les itérateurs sont prévu pour fonctionner dans l'intervalle semi ouvert [begin ; end [, il fallait bien leur donner la possibilité de représenter... un élément invalide :aie:
Bien sur que les pointeurs font partie intégrante du langage et de la bibliothèque standard.Citation:
Donc, les pointeurs font partie intégrante de la librairie standard C++.
Ne serait-ce que parce qu'autrement, tu n'aurais pas la facilité actuelle d'interfaçage du C avec C++.
Mais les références font, aussi, partie intégrante du langage et de la bibliothèque standard.
Et, comme les références ont des cas d'utilisation identiques aux pointeurs, mais que les pointeurs présentent malgré tout des cas d'utilisation que les références ne présentent pas, le mieux est encore d'utiliser les références "chaque fois que possible" et les pointeurs "lorsque l'on n'a pas le choix" ;)
De cette manière, tu sécurise à peu de frais toute une plage de situations dans laquelle tu aurais du sécuriser toi-même l'utilisation des pointeurs.
A quoi te servirait de vouloir modifier une variable anonyme temporaire :question:Citation:
Et ça ne marche qu'avec une référence constante, donc l'objet sera non-modifiable. Et si je veux le modifier? J'en reviens encore à utiliser un pointeur.
Quel intérêt aurais tu, avec une fonction proche de
quand elle est appelée sous la forme deCode:
1
2
3
4 void foo(std::string const & str) { /* tu fais ce que tu veux de str */ }
De plus, comme je l'ai dit, le concept de const-correctness t'apporte, dés la compilation, la certitude que tu ne modifie pas des choses que tu ne peux pas ou que tu ne veux pas modifier.Code:
1
2
3
4 int main() { foo("bonjour"); }
Il serait dommage de se passer de cette possibilité :aie:
Si, pour une raison ou une autre, tu veux, effectivement, pouvoir modifier un objet issu de la fonction appelante dans la fonction appelée, tu reste tout à fait libre d'utiliser une référence non constante, mais, il n'y a alors aucun sens à vouloir utiliser une variable anonyme temporaire...
Oui, mais tu as la certitude qu'il existe... Certitude que tu ne peux obtenir que par un test lorsque tu manipule un pointeurCitation:
De toute façon, ce n'est qu'un raccourci syntaxique; l'objet en question doit bien exister quelque part pour être passé en paramètre, et il va en fait être sur la pile comme tous les autres.
Et surtout d'apporter une sécurisation largement supérieure à celle que tu obtient "par défaut" (sans intervention de l'utilisateur) avec les pointeurs.Citation:
Donc, je te concède encore une fois que les références permettent des raccourcis syntaxiques intéressants dans certains cas.
Et comme cette sécurisation te permet d'éviter les tests d'existence que tu es tenu de mettre en place avec les pointeurs, tu gagne "sur les deux tableaux" que sont les performances et la sécurité...
On a, alors, des expériences tout à fait différentes...Citation:
Mais dans mon expérience, ces cas sont loin de former la majorité et il arrive si souvent qu'on doivent changer une référence pour un pointeur que l'on est mieux de toujours utiliser des pointeurs, simplifiant la syntaxe (plus lourde oui mais plus cohérente et explicite) et d'éviter ainsi de perdre du temps.
Parce que les cas dans lesquels j'ai du décider de remplacer une référence par un pointeurs sont particulièrement rares.
Et, attention, je ne dis pas qu'il faut commencer par coder sous la forme d'une référence, et décider, parce que l'on n'a pas d'autre choix, de remplacer la référence par un pointeur, mais je dis qu'il faut, dés le départ, réfléchir à ce que l'on fait, et utiliser une référence "chaque fois que possible" et un pointeur "quand on n'a pas le choix"...
C'est quand même différent ;)
C'est parce que ces langages travaillent de manière tout à fait différente...Citation:
Or le concept d'une "référence qui ne peut pas être nulle" n'existe pas dans la plupart des langages: Java, C#, Python, etc. Et pourtant on peut garantir qu'une fonction ne retournera pas nul dans ces langages: en lançant une exception en cas d'erreur.
Java et C# utilisent un système "externe" de gestion de la durée de vie des objets, et tu ne peux pas décider, comme en C++, de prendre la resonsabilité de cette gestion ou non.
Et cela ferait double emploi...Citation:
La même chose est possible en C++, et il n'y a donc pas besoin d'une "référence qui ne peut pas être nulle". Je peux retourner un pointeur si tout va bien, et lancer une exception en cas d'erreur.
Les pointeurs présentent, effectivement, l'intérêt majeur de pouvoir être nuls.
Cela signifie que, si tu as une fonction qui renvoie un pointeur, tu va, naturellement, envisager un code proche de
simplement parce que, en bon programmeur C, tu auras pris l'habitude de vérfier qu'un pointeur récupéré par ailleurs n'est pas nul, alors que ce code n'a aucun intérêt si bar lance une exception en cas de nullité du pointeur :aie:Code:
1
2
3
4
5
6
7
8 void foo() { Type * ptr = bar(); if(ptr != NULL) { /* ce qu'il faut faire */ } }
Là n'est pas le problème...Citation:
Ma fonction garantit donc que le pointeur sera non-nul. Le fait d'utiliser une référence ici est sans aucune utilité! Si j'utilise une référence, le code de la fonction sera le même à 2 caractères près (&*), et la sémantique de la fonction sera exactement la même.
Par contre, le fait que tu doive faire une différence dans ton code parce que tu manipules un pointeur peut parfaitement en être un, alors que, justement, tu garde la possibilité d'utiliser une référence comme s'il s'agissait de l'objet d'origine, et que c'est ce qui arrive réellement: les actions que tu effectue au départ d'une référence sont, en réalité, effectuées au départ de l'objet référencé.
Là, pour une fois, je suis tout à fait d'accord avec toi :DCitation:
En fait nous sommes en désaccord fondamental ici. :P
Là, par contre, je suis, effectivement en total désaccord avec toi.Citation:
C++ sert les mêmes buts que le C, à plus grande échelle. Il est essentiellement implémenté comme un superset du C.
C et C++ sont deux langage totalement différents et, bien que l'on retrouve dans C une grosse partie de ce que l'on trouve en C, ce n'est pas une raison pour développer en C++ en utilisant les possibilités issues du C, essentiellement parce qu'il existe en C++ des possibilités similaires qui apportent une sécurisation largement plus importante.
Il est déjà "suffisamment facile" de se "tirer une balle dans le pied" en C++ pour que l'on tente d'éviter les problèmes que l'on peut éviter en préférant les possibilités issues du C++Si ce n'est que C est un langage exclusivement séquentiel, alors que C++ est un langage multi paradigme (séquentiel, certes, mais aussi objets et générique).Citation:
C++ est un langage de gestion manuelle de mémoire et la syntaxe des pointeurs du C est parfaitement adaptée à cette tâche.
Les deux paradigmes supplémentaires nous permettent de travailler mieux et de manière plus sécurisante, même lorsque l'on travaille en séquentiel, il serait donc dommage de se priver de ces possibilités, même s'il est, effectivement, possible de le faire.
Non...Citation:
(D'ailleurs C# l'a repris pour le code "unsafe".) Les références ne servent qu'à permettre certains raccourcis syntaxiques et à donner l'impression que C++ est plus orienté-objet qu'il ne l'est, au prix d'une duplication de fonctionnalités, de l'introduction d'une ambiguïté syntaxique, et de discussions comme celle-ci. :mrgreen:
C++ est réellement orienté objets. Il n'est, effectivement, pas exclusivement orienté objet, mais c'est, quelque part, ce qui fait sa force comparé à des langages qui ne le sont absolument pas (C++ en tête) ou qui le sont à outrance (java en tête).
Tu semblais pourtant dire le contraire :aie:Citation:
Nous sommes d'accord. Je ne fais pas de différence entre "un cas d'exception" et "une erreur". Une exception peut être utilisée partout où on veut retourner une erreur.
Un pointeur ferait, effectivement, exactement la même chose, mais la simplicité serait apportée au concepteur, alors que le fait de renvoyer end complexifie, peut être, la tâche au concepteur, mais facilite celle de l'utilisateur.Citation:
Parfois, cependant, on peut très bien vouloir retourner nul sans que cela représente une erreur. Par exemple, une fonction de recherche ne devrait pas lancer d'exception si elle ne trouve pas l'objet voulu, mais bien retourner nul. C'est ce que fait std::find d'ailleurs; mais comme std::find retourne une référence, il se voit contraint de retourner une référence sur un "objet invalide" (l'itérateur end() ou npos), et là on nage en plein ridicule, puisqu'un pointeur ferait exactement la même chose de façon beaucoup plus simple.
En effet, toutes les collections de la STL présente une interface commune, composée des fonctions membres begin et end.
Cela signifie que tu peux respecter beaucoup plus facilement OCP (Open Close Principle: le fait qu'un code doit être ouvert à l'évolution et fermé à l'exécution), simplement parce que, avec un code proche de
tu pourra décider à n'importe quel moment, et pour des raisons variées, de remplacer ton std::vector par n'importe quelle autre collection "compatible".Code:
1
2
3
4 void foo() { for_each(truc.begin(),truc.end(), functor()); }
La notion d'itérateur, et les fonctions membres associées, est, simplement, une notion supplémentaire (même si elle s'utilise comme un pointeur) qui permet de faire en sorte que, pour autant que tu garde une interface compatible, tu puisse ne pas t'inquiéter de la collection qui sera réellement manipulée.
Et cela rentre en réalité non pas dans le domaine de la programmation objets, mais bel et bien dans celui de la programmation générique ;)
Attention, je suis le premier à dire que C et C++ sont des langages particulièrement complexes, avec lesquels il est des plus simples de se tirer une balle dans le pied.Citation:
Je suis entièrement d'accord, et comme tu comprends cette différence tu as de bonnes chances d'utiliser correctement les pointeurs intelligents. Mais j'ai trop souvent entendu dire qu'ils permettaient d'abstraire la gestion de mémoire comme en Java, alors que c'est tout à fait faux. C'est un outil qui aide dans certains cas mais qui n'offre aucunement les caractéristiques de performance et de sécurité de Java. En C++ la seule entité qui peut gérer correctement la mémoire, c'est le programmeur, et s'il ne comprend pas comment les outils qu'il utilise fonctionnent, c'est le désastre assuré.
Et je suis d'accord avec toi qu'on ne peut pas laisser qui que ce soit croire que les pointeurs intelligents présentés par C++ permettent ce que le Garbage Collector permet en java ou en C# ;)
Absolument pas...
D'abord, comme l'a fait valoir Joel, tu as certains systèmes qui fournissent un log "matériel", mais, surtout, il faut comprendre que new[] essaye d'allouer de l'espace contigu en mémoire.
Or, ce n'est pas parce que le système est incapable de trouver XXX bytes contigus en mémoire qu'il sera d'office incapable de trouver XXX-1 bytes :aie:
De plus, les chaines de caractères std::string n'utilisent pas forcément un espace contigu de mémoire pour représenter l'ensemble des caractères qui les composent.
Seule sa fonction membre c_str() présente ce type de limitation (étant donné qu'elle renvoie, effectivement, une chaine C style):aie:
Il se peut, effectivement, que tu n'ai pas la possibilité d'allouer la mémoire nécessaire à la représentation interne de ta chaine, mais c'est loin d'être forcément le cas ;)
reccord du post le plus long imo
Certainement pas. Les librairies standard C++, par exemple, n'exposent que des pointeurs bruts (i.e. string::c_str()). Les pointeurs intelligents pourraient être utiles dans l'implémentation de cette librairie, et peut-être peuvent-ils utiles dans l'interface publique, mais je n'ai pas encore rencontré ce cas.
Je vais essayer de répondre à koala maintenant en faisant un peu plus court que la dernière fois. ;)
Premièrement, la fameuse "garantie" de non-nullité des références est une chimère, puisqu'une référence peut très bien être invalide. Tous les débutants font l'erreur suivante:
Tu me diras peut-être que le compilateur devrait émettre un warning, mais il émettrait le même warning si un pointeur avait été utilisé à la place. Ou encore, si je prends une référence sur un objet alloué sur le tas, et qu'il est détruit, ma référence est encore une fois invalide. Donc, une référence n'apporte aucune garantie réelle par rapport à un pointeur. Et c'est encore pire parce que je n'ai aucune façon de tester la validité d'une référence, tandis qu'un pointeur sera typiquement nul.Code:
1
2
3
4
5 string& MyFunction() { string a("Hello!"); return a; }
Mais end() pourrait aussi bien être nullptr et cela pourrait constituer l'interface commune de la STL. Ce serait plus simple puisque cela utiliserait une fonctionnalité du langage plutôt que de requérir une nouvelle fonction de librairie. Dans tous les langages orientés-objets et en C, une fonction de recherche qui ne trouve rien retourne un "pointeur nul". Cela n'empêche pas d'avoir des interfaces communes.Citation:
Un pointeur ferait, effectivement, exactement la même chose, mais la simplicité serait apportée au concepteur, alors que le fait de renvoyer end complexifie, peut être, la tâche au concepteur, mais facilite celle de l'utilisateur.
En effet, toutes les collections de la STL présente une interface commune, composée des fonctions membres begin et end.
Le fait que std::find retourne end(), ou string::npos dans le cas d'une string, est une complexité supplémentaire non seulement pour le concepteur mais aussi pour l'utilisateur de la librairie, qui doit apprendre une fonction et un objet statique supplémentaire plutôt que d'utiliser la fonctionnalité du langage qui devrait intuitivement servir à cette tâche. Encore une fois, end() et npos ne font qu'émuler le pointeur nul: c'est sémantiquement la même chose! Or ré-implémenter dans une librairie ce qui est déjà offert par le langage, c'est nager en plein absurde.
Je comprends très bien ce que sont les pointeurs et les références, merci. ;)Citation:
Ce que tu dois comprendre, c'est qu'un pointeur est un type de variable particulier en cela qu'il contient... l'adresse mémoire à laquelle se trouve l'objet pointé, alors qu'une référence n'est qu'un alias, un autre nom, pour la variable d'origine.
De ce fait, je vais dire que tu t'en fous royalement du fait que tu travaille ou non avec une référence: cela s'applique, au final, exactement sur le même objet.
Il n'y a strictement aucune raison de faire une différence entre une référence et l'objet d'origine, simplement, parce que c'est la même chose.
Il y a deux bonnes raisons de vouloir différencier entre un objet et une référence en C++. La première est qu'une référence peut être invalide, comme je l'ai illustré plus haut, alors qu'un objet, non. La deuxième est que la syntaxe générale pour "référencer" en C++, celle qui s'applique dans 100% dans des cas, est différente, et qu'on introduit donc une confusion (une ambiguïté) en utilisant une autre syntaxe pour représenter la même chose.
Non! J'ai illustré ici qu'une référence peut être invalide (ce qui revient au même), et j'ai illustré dans mon message précédent qu'on peut garantir qu'un pointeur sera non-nul. Donc, tout ce que tu gagnes avec une référence, c'est une certaine légèreté syntaxique dans certains cas.Citation:
Tu ne peux absolument pas donner une telle garantie avec un pointeur, alors qu'avec une référence, elle est implicite et non contournable.
Tu sembles impliquer que tout pointeur doit toujours être testé pour savoir s'il est non-nul. Or, en Java par exemple (et ça n'a rien à voir avec le fait que la mémoire est gérée dans ce langage), toute référence peut être nulle en tout temps. Et pourtant, on ne passe pas son temps à écrire if (untel == null). Et on arrive à écrire des programmes qui marchent très bien quand même.
Et en fait, il n'existe aucun langage, autre que C++, avec un concept de "référence qui ne peut pas être nulle". Et pourtant, on ne passe pas son temps à tester la nullité des références dans ces autres langages (sauf le C, j'y arrive), et on arrive à écrire des programmes tout à fait corrects. Donc, contrairement à ce que tu sembles dire, utiliser des pointeurs à la place ne veut pas dire "tester la nullité à tout bout de champ".
Tu argumentes que les références t'apportent une sécurité à peu de frais. Or la façon dont tu garantis que tes références sont valides, c'est par les exceptions. Donc, ce sont les exceptions qui t'apportent une sécurité, et non les références. Par exemple, personne ne teste la nullité de ce que "new" retourne, parce que "new" garantit la non-nullité du pointeur à l'aide d'exceptions. En codant à l'aide d'exceptions, tu n'as pas besoin de vérifier la non-nullité des pointeurs, comme on le fait en Java, comme on le fait en C#, etc.
La seule raison pour laquelle en C, on teste souvent la nullité des pointeurs, c'est l'absence d'exceptions, et non la sémantique des pointeurs. Ce qui permet d'éviter ces tests, ce ne sont pas les références C++, ce sont les exceptions, comme le démontrent Java et les autres langages supportant les exceptions.
C'est comme si C++ définissait un type int qui ne peut pas valoir -1 (souvent utilisé comme code d'erreur en C). Que faudrait-il faire? À moins d'avoir un autre mécanisme de gestion d'erreurs (les exceptions), on n'aurait d'autre choix que de continuer à utiliser l'ancien type int. Mais comme les exceptions existent, et qu'on n'a plus besoin de retourner -1, ceux qui souscrivent à ta logique diraient: il faut utiliser le nouveau type int car il garantit que la valeur retournée ne sera pas une erreur. C'est la même chose!
Prenons l'exemple que tu donnais avec une const string&. Je pourrais vouloir insérer quelque chose au milieu de la string et ensuite l'afficher. Ce n'est pas trop demander j'espère? ;) Or, impossible si la string est const. Je suis contraint d'en créer une copie, ce qui est ridicule. Donc, j'enlève le const, et je ne peux plus passer une variable temporaire.Citation:
A quoi te servirait de vouloir modifier une variable anonyme temporaire?
En fait, ce qui est ridicule, c'est d'essayer de garantir l'immutabilité d'une variable temporaire. À quoi cela pourrait bien servir? Puisque c'est une variable temporaire, l'appelant ne pourra jamais y accéder après l'appel, justement, donc qu'est-ce que ça peut bien lui faire si elle a été modifiée? :lol:
Je ne comprends pas ce que tu veux dire. new lance une exception s'il ne peut pas allouer de mémoire. Si aucune exception n'est lancée, tu as donc la garantie que ta "référence" est non-nulle. La référence sera invalidée quand l'objet sera détruit, oui, mais c'est ce qui arrive déjà de toute façon! Si je prends une référence sur un objet alloué dynamiquement, et que cet objet est détruit, je me retrouve encore une fois avec une référence invalide. Le fait que new retourne un pointeur ou une référence ne changerait rien à cette situation.Citation:
On ne peut donc pas faire en sorte que new renvoie une référence, simplement parce qu'il renverrait... une référence sur un objet invalidé du fait de sa destruction automatique.
Pas du tout. J'ai dit "on ne devrait retourner une exception que dans un réel cas d'exception". Une fonction de recherche qui ne trouve rien n'est pas un réel cas d'exception, ni une "erreur": cela fait partie des résultats normaux d'une recherche.Citation:
Tu semblais pourtant dire le contraire
Ce sera une autre discussion, parce que là je ne suis pas du tout d'accord mais on serait hors sujet. :PCitation:
C++ est réellement orienté objets.
je n'ai pas franchement envie de tout quoter, amsi il me semble que dans le cas ou tu dis qu'un objet pointé par une référence peut être effacé et rendre la référence invalide, je dirai que dans ce cas c'est equivalent au pointeur (nu).
Or il est assez souvent admis que les poineturs nus sont source d'erreur pour le peux de garantie qu'ils proposent. Je les mets personnellement au meme rang que les références (que je n'aime pas) justement pour cela.
Mais de la a dire que la SL utilise des pointeurs nus, je trouve ca un peu limite. En effet, a part c_str(), je veux bien voir un autre exemple de pointeur nu dans la SL :)
la SL utilise plutot des semantiques de valeur, privilégie donc les copies et les références.
Les pointeurs ne deviennent utiles que pour des sémantiques d'entité. Et la c'est sans doute l'historique qui pousse a utiliser des pointeurs nus. Ou la disparité des implémentations. Qt a par exemple sa propre implémentations, boost la sienne, etc
Ca ne les empêche pas d'utiliser des pointeurs nus mais la je trouve que c'est une erreur. Ils utilisent les poineturs nus comme des "weak ptr", comme des références en quelque sorte.
Mais quoi qu'il en soit, dans ces bibliotheques utiliser un pointeur nu c'est s'exposer aux leaks mémoire ou crashs d'acces memoire, la ou les smart pointeurs sauveraient bien des vies. C'est un peu se faire du mal pour rien.
En conséquences je pense que les references et les pointeurs ne sont pas utilisées de la meme facon; une référence s'utilise avec une semantique de valeur, un pointeur avec une semantique d'entité, et les r´férences sur les entités sont au mieux confuses, et les pointeurs nus sur les entités simplement a proscrire.
Ce que retourne c_str est un chaine de caractère C-like, alors c'est évident que ca ne peut pas être un pointeur intelligent. Les cas de retour de pointeur brut sont quand même pas si courant dans la S(T)L.
Pour le point que tu soulèves à propos des variables temporaires que l'on veut modifier, c'est un problème bien connue que la sémantique de "mouvement" du C++1x devrait corriger il me semble.
Pour les cas d'invalité des références que tu cites, ils sont bien réels. Cependant, ce sont des cas qui ne devrait pas arriver dans un programme bien réalisé, alors que des pointeurs nuls ca arrive à foison, raison pour laquel il faut les tester. Ces cas de référence invalide doivent faire partie de ce koala appelle "les possibilités du C++ de se tirer une balle dans le pied."
Pour ce qui est de la décision d'un symbole particulier pour signifier la fin d'une séquence, je trouve que c'est bien plus "expressif" que de retourner un pointeur sur NULL, qui permettrait aussi de faire des codes dangereux interdits pour le moment. (les itérateurs ont la sémantique des pointeurs mais sont quand même bien plus adapté pour itérer dans un conteneur, plus simple à incrémenter et à vérifier les bornes).
[EDIT] Pas d'accord avec ce que tu soulignes en gras, les exception m'assure juste que la fonction s'est bien déroulé, en aucun cas que le pointeur est non nul. Si je vois une fonction avec un pointeur, je suppose directement que le choix d'un pointeur a été fait pour avoir une valeur NULL, et que donc je dois tester avant de déréférencer. [/EDIT]
Le C++ permet la définition d'objet, ie d'un ensemble de donné qui peut réagir à des messages, le polymorphisme avec un typage statique, je ne vois pas en quoi il ne serais pas orienté objet. Il ne fait peut-etre pas les meilleur choix (dispatch naturel impossible par exemple) mais ca reste OO.
référence null != dangling référence.Citation:
Premièrement, la fameuse "garantie" de non-nullité des références est une chimère, puisqu'une référence peut très bien être invalide.
Et _oui_ une réfèrence ne peut pas être NULL c'est garanti par le standard.
+1Citation:
Ce que retourne c_str est un chaine de caractère C-like, alors c'est évident que ca ne peut pas être un pointeur intelligent. Les cas de retour de pointeur brut sont quand même pas si courant dans la S(T)L.
Et
Oui (on notera quand même la présence de copy elision et du (N)RVO (non ce n'est pas la même chose) en C++98)Citation:
Pour le point que tu soulèves à propos des variables temporaires que l'on veut modifier, c'est un problème bien connue que la sémantique de "mouvement" du C++1x devrait corriger il me semble.
Je voulais simplement dire que la STL n'expose pas de pointeurs intelligents, mais que lorsqu'elle expose un pointeur, c'est un pointeur brut, en réponse à la question de el_socio.
J'aimerais bien avoir un exemple de cas où une référence apporte une sécurité supplémentaire à un pointeur, parce que bien honnêtement je ne vois pas pourquoi un pointeur serait plus facile à invalider qu'une référence. Parce qu'une référence est non-réassignable? Un pointeur const ne l'est pas davantage. Enfin, je suis curieux, éclairez-moi donc.Citation:
Pour les cas d'invalidé des références que tu cites, elles sont bien réel. Cependant, ce sont des cas qui ne devrait pas arriver dans un programme bien réalisé, alors que des pointeurs invalidés ca arrive à foison, raison pour laquel il faut les tester. Ces cas de référence invalide doivent faire partie de ce koala appelle "les possibilités du C++ de se tirer une balle dans le pied."
La STL a la syntaxe la moins expressive de toutes les librairies de conteneurs que je connais. Être obligé d'écrire:Citation:
Pour ce qui est de la décision d'un symbole particulier pour signifier la fin d'une séquence, je trouve que c'est bien plus "expressif" que de retourner un pointeur sur NULL, qui permettrait aussi de faire des codes dangereux interdits pour le moment.
, juste pour traverser un conteneur, c'est le contraire même de l'expressivité. L'existence de BOOST_FOREACH ne fait que le prouver.Code:
1
2
3 for (letypedemonconteur::const_iterator it = monconteneur.begin(); it != monconteneur.end(); ++it) { }
Donc tu testes si "new" retourne nul? Inutile, puisqu'il retourne une exception en cas d'erreur. Quand tu programmes en Java, tu testes si chaque référence est nulle? Bien sûr que non, puisqu'encore une fois une fonction bien codée rapporte une erreur par une exception plutôt qu'une pointeur nul. Et qu'est-ce qui te peut te garantir qu'une référence est valide, sinon les exceptions?Citation:
Pas d'accord avec ce que tu soulignes en gras, les exception m'assure juste que la fonction s'est bien déroulé, en aucun cas que le pointeur est non nul. Si je vois une fonction avec un pointeur, je suppose directement que le choix d'un pointeur a été fait pour avoir une valeur NULL, et que donc je dois tester avant de déréférencer.
Mais un crash == un crash. :zoubi:Citation:
référence null != dangling référence.
@flob : Ce que je voulias dire c'est que la copy elision c'est plus large que ça, ie ça se résume pas au (N)RVO. (et au passage c'est 12.8.15, t'utilises un draft ?)
Le prix de la généricité faut croire. Et je suis pas d'accord, la syntaxe est certes lourde, mais pas inexpressive.Citation:
La STL a la syntaxe la moins expressive de toutes les librairies de conteneurs que je connais. Être obligé d'écrire:
Je parle d' "expressivité" dans le sens le plus commun quand on parle de langages de programmation, c'est-à-dire une mesure des idées qui peuvent être exprimées de façon concise et lisible. (lien)
[références vs pointeurs]
Nous avons une différence philosophique d'utilisation du langage qui est à la racine de la différence pointeur vs référence quand on a le choix.
Pour plusieurs d'entre-nous ici, tout ce que le compilo peut vérifier pour nous à la compilation, on prend!
Les rares cas où je vais accepter un pointeur sachant que je lui interdis d'être nul (via assert -- et JAMAIS une exception pour traiter une erreur de prog), c'est parce que sémantiquement je vais vouloir tisser un lien vers un autre objet. C'est aussi pour ça que le fait que this soit un pointeur ne me choque pas : pour pouvoir tisser des liens.
Sinon, je ne cherche pas à comprendre, je forcerai systématiquement le paramètre à être une référence. Un boulet peut m'avoir refilé une référence invalide ? Je commence par le fouetter pour cette erreur de prog et je corrige son erreur et je continue mon chemin.
Côté attributs, c'est exactement la même histoire. Je rends statiques tous les invariants que je connais par construction du programme (à prendre dans le même sens que "toto a telle propriété par construction" que l'on connait en maths).
Dans la continuité j'ai très rarement des constructeurs par défaut, et encore plus rarement des mutateurs.
Aujourd'hui, je bouche le bouchon à écrire "const int i = f(params);". Si si. D'un point de vue maintenance, c'est génial. Là où je vois la déclaration, j'apprends que la valeur ne changera plus. C'est un truc de moins sur lequel réfléchir. Une possibilité de moins que l'on laisse aux reprenneurs pour faire n'importe quoi et altérer l'entrée que l'on utilise en précondition/invariant plus loin.
Les références, c'est la même chose: une garantie statique.
[exception, nullité et erreurs de programmation]
Je reviens là-dessus car je sens que nous touchons ici une autre différence dans nos visions.
Un erreur de programmation, je vais systématiquement la surveiller avec des assertions. C'est mal-dit. Mes invariants et mes préconditions liés à une mauvaise conception/élaboration d'algo/potentielle erreur , je vais les valider avec des assertions en second lieu. [en premier lieu je m'en remets au compilo].
En troisième lieu, j'ai des tests unitaires. Et enfin les tests de validation, intégration, et cie.
Les assertions ont un énorme avantage sur les exceptions : les core dump. C'est un fantastique outil qui nous renvoie le contexte exact de l'erreur (snapshot de la pile de chaque thread + tas + statiques/globales) à contrario des exceptions qui au mieux vont nous renvoyer la liste des fonctions dans la pile et sans les variables.
Pour en revenir aux références, on (je m'inclue avec les autres) ne garantit pas leur validité par exception, mais par construction (au sens des démonstrations en maths). C'est très différent.
[Il existe bien un type int qui ne renvoie jamais -1, c'est unsigned int qui m'offre un garantie sémantique : il n'est jamais négatif ; c'est malheureusement difficile à assurer par construction à la différence de la const-correction, et des garanties des références (où il faut vraiment le faire exprès pour renvoyer un truc invalide -- et référence ou pointeur, on ne pourra savoir savoir quand il est dandling de totues façons).
[Concernant la MT-safety des shared_ptr<>]
Euh ... C'est quoi le problème ? Les shared_ptr les mêmes garanties que les pointeurs bruts, plus une petite garantie relative à l'incrément des références. Partager avec possibilité de modification une même variable pointeurs entre plusieurs threads me parait le b-à-ba du n'importe quoi, au contraire de partager une même ressource qui sera derrière un pointeur à comptage de référence incrémentée atomiquement.
[Pour la perf de new/delete]
... pools ? Ce que fais Java en natif, quoi. A sortir quand les benchmarks nous disent que le problème est là. Un faux débat sinon.
[bad_alloc]
@el socio, j'ai déjà eu un new qui a échoué -- un resize() en fait, mais c'est pareil. Un couillon de pair qui me balançai un longueur de message négative sur notre socket et moi qui allouait innocemment. Heureusement je ne plantais pas, mais je coupais la connexion correspondante, ce qui était à peine mieux.
[Et cette situation n'engendrait pas de fuite pour autant, vu comme il est simple d'être leak-free avec le RAII. Avoir un serveur robuste qui ne s'arrête jamais, je n'appelle pas ça être universitaire.]
Salut Luc Hermitte,
Pourquoi? Je me donne la liberté de remettre cette idée en question, puisque les idées pré-reçues sont l'ennemies de la logique. L'idée derrière cette "règle" est que les erreurs de compilation sont plus faciles à régler que les erreurs d'exécution.Citation:
Pour plusieurs d'entre-nous ici, tout ce que le compilo peut vérifier pour nous à la compilation, on prend!
Or, en C++, les erreurs qui m'ont fait perdre le plus de temps sont des erreurs de compilation, surtout lorsqu'on prend en compte qu'il est exceptionnellement long de compiler un programme C++ et donc d'obtenir ces erreurs. J'ai appelé à mon aide des programmeurs d'expérience (+ de 20 ans d'expérience en C++, difficile de faire mieux), et ils n'ont pas trouvé. J'ai perdu des semaines à faire compiler du C++, et je ne suis pas le seul. Quand on sait à quel point les messages d'erreurs de compilateurs peuvent être incompréhensibles surtout quand on utilise Boost et autres casses-têtes de templates, que le compilateur peut simplement planter, sans la moindre indication, à cause d'une faute de frappe indiscernable, on en vient à préférer parfois les erreurs à l'exécution, erreurs que les tests unitaires détectent de façon méthodique et fiable.
Un peu d'expérience dans d'autres langages ne fait pas de tort non plus. En Python, par exemple, il n'existe pas de "const". Il n'y a pas de "référence qui ne peut pas être nulle". Et pourtant, des systèmes complexes et très fiables sont développés en Python, bien plus rapidement qu'en C++ d'ailleurs. Après avoir écrit un programme de taille moyenne en Python, on revient en C++ et on se dit "mais pourquoi je me donne tout ce mal?"
Et donc, je n'accepte pas comme idée reçue que "tout ce que le compilateur peut vérifier, on le prend". Malheureusement les programmeurs C++ prennent trop souvent le C comme point de comparaison: "mieux que le C", ce n'est pas encore si formidable que ça.
Je suis bien d'accord qu'on ne doit pas camoufler un bogue par une exception. Or, un programme n'interagit pas qu'avec lui-même, mais aussi avec le monde extérieur, qu'il ne contrôle pas. Et là où les hypothèses qu'il fait sur le monde extérieur échouent, il rencontre une situation d'exception et se voit incapable de respecter sa propre spécification, en quel cas il devrait rapporter la situation aux instances supérieures (le client) par le moyen d'une exception.Citation:
Pour en revenir aux références, on (je m'inclue avec les autres) ne garantit pas leur validité par exception, mais par construction (au sens des démonstrations en maths). C'est très différent.
Pour prendre la STL comme exemple encore une fois, la fonction vector::at(int index) retourne une référence sur l'objet situé à index, et lance une exception sinon. Donc, elle garantit la validité de la référence à l'aide d'une exception. Je ne vois pas comment la STL pourrait éviter de lancer une exception par "construction" étant donné que l'entrée ici (l'index) vient du monde extérieur (l'utilisateur) et est par conséquent incontrôlable. Si la STL ne lançait pas d'exception, elle serait contrainte de retourner soit une référence invalide (affreux), ou un pointeur nul (un peu mieux), ou un code d'erreur (...) mais le mieux est de lancer une exception.
Comme j'ai dit, shared_ptr est plus thread-safe que je me le représentais initialement, et est donc une solution viable dans un contexte multi-threads, je suis d'accord.Citation:
Partager avec possibilité de modification une même variable pointeurs entre plusieurs threads me parait le b-à-ba du n'importe quoi
Il est tout à fait possible d'avoir un système de gestion mémoire similaire à Java en C++, mais ce n'est pas les pointeurs intelligents qui l'implémentent. Le danger est de croire que oui. C'est tout ce que j'ai dit.Citation:
[Pour la perf de new/delete]
... pools ? Ce que fais Java en natif, quoi. A sortir quand les benchmarks nous disent que le problème est là. Un faux débat sinon.
Pour ce qui est de ce qui vient du monde extérieur, tu cites at mais son comportement est exceptionnel dans la STL, la philosophie de la STL est plutôt : tu ne respectes pas les pré-requis, alors moi je fais n'importe quoi, c'est comme ca que fonctionne [] et la plupart des fonction de la STL.
Pour l'idée de ce qui est vérifiable à la compilation est bon à prendre, je ne vois pas tes arguments contre : tout ce qui est fait à la compilation est fixé, ca ne varira plus, c'est une garantie forte que ces points seront justes.
Si j'arrive à prouver (ou vérifier avec le compilo) qu'une de mes variables est inclus dans un certain intervalle, je vois difficilement pourquoi je ne considérerais pas cette information et reppouserais cette vérification à l'execution, à part une perte de perf je vois aucun avantage.
Pour la verbosité des erreurs en C++, je suis bien d'accord, surtout quand les template et la STL s'en mêlent, mais je ne pense pas que ce soit un argument pour dire que les vérifications à la compilation sont une mauvaise chose, surtout qu'il me semble qu'il existe des outils pour rendre ces messages plus clair.
a- ce n'est pas une idée préconçue, mais l'état actuel de mon opinion sur le sujet, opinion induite par mes expériences.
Je perds plus de temps à traquer des dérèglements induits par les laxismes des C&C++ qu'à comprendre les erreurs de compilation qui me sont sortis.
Et franchement, ce ne sont pas quelques const, quelques références, voire des volatiles (cf le détournement du mot clé par A.Alexandrescu), et autres invariants d'immuabilité pour vont me causer des complications pour compiler.
b- 20 d'expérience en C++ n'est pas exactement un critère au vu des révolutions perpétuelles dans ses paradigmes :P (les templates ont moins de 15 ans, IIRC, et la meta-prog template source de tant de difficultés encore moins)
Ceci dit, j'ai beaucoup de respects pour certains développeurs qui ont cette expérience dans ce langage également. Je n'ai jamais autant appris qu'en fréquentant fclc++.
J'y pense, pour boost c'est insuffisant (il faudra, disons, rajouter les substitute qui vont bien), mais pour la SL, STLfilt est notre très précieux ami. Je suis régulièrement surpris par le faible pourcentage de développeurs C++ ayant croisé le chemin de cet indispensable outil.
c- Les TU ne me servent qu'à vérifier les algos en C++. C'est une chance que nous avons comparativement aux langages interprétés où il faut cette fois vérifier bien plus de choses.
d- Autre expérience : GWT qui vante les mérite de la compilation. Et j'ai vraiment été convaincu après expérimentation.
La force de python, se sera de pouvoir prototyper rapidement, si on le compare au C++. Mais le temps que l'on aurait pu passer à prendre un café, on va l'avoir perdu à développer des TU qui se substituent à la phase de compilation -- et qui ne remplacent pas les TU vérifiant les algos.
e- Oh que non. Le C++ est loin d'être le seul langage que nous connaissons. Comment le maitriser si on ne connait que lui et le C?
Avec tous mes const et autres "les mutateurs ne passeront pas moi", j'expérimente justement des propriétés tirées de langages où la mu(t?)abilité n'existe pas.
f- nous sommes d'accord -- de même que pour les parties de la fin que je n'ai pas citées.
g- Je n'utilise jamais at(). Mais vraiment. Jamais. Je la vois comme une fonction pour faire plaisir. En ce qui me concerne, elle n'existe pas. Mon seul point d'entrée est operator[], et j'ai une nette préférence pour ses implémentations qui claquent une assertion sur dépassement de bornes -- pour la raison que j'ai citée: se planter dans les bornes, c'est une erreur de programmation. Malheureusement, ce n'est pas spécifié ainsi.
Par construction mon indice est toujours valide avant de demander quoique ce soit au vecteur, soit parce que j'ai fait une vérification sur une entrée non maitrisée, et donc je pars faire autre chose, soit parce que je suis dans un contexte très précis : boucle, résultat de find, etc.
Disons que at() c'était un mauvais exemple. Disons aussi que quand je sais qu'une fonction peut ne pas renvoyer un truc, alors je vais renvoyer un pointeur. Et sans hésiter (j'ai dit "quand on a le choix", non ? bon, c'est vrai que nous aurions le choix, mais ça serait maladroit de mon avis) (à part avec un itérateur/indice suivant ce que je dois faire ensuite).
OK, je vois le rapport en exception et référence dans ta prose. Pointeur je prends ici dans les situations de recherche. at() n'existant pas ... :roll:
Le fait est que, même s'il faut, effectivement, avouer que certaines erreurs de compilation sont particulièrement difficilement compréhensibles, une fois que tu as corrigé l'erreur de compilation, tu as fini avec cela, alors qu'une erreur de logique ou d'exécution, elle peut survenir n'importe quandIls faut aussi voir lesquels...Citation:
J'ai appelé à mon aide des programmeurs d'expérience (+ de 20 ans d'expérience en C++, difficile de faire mieux),
Certains programment encore exactement comme il le faisaient il y a 20 ans, et ce ne sont donc pas forcément des références :aie:Et pourtant, j'en suis arriver (comme de nombreux autres ici, d'ailleurs) à configurer mon compilateur pour qu'il me sorte un maxium d'avertissements et pour veiller à tous les prendre en compte...Citation:
et ils n'ont pas trouvé. J'ai perdu des semaines à faire compiler du C++, et je ne suis pas le seul. Quand on sait à quel point les messages d'erreurs de compilateurs peuvent être incompréhensibles surtout quand on utilise Boost et autres casses-têtes de templates, que le compilateur peut simplement planter, sans la moindre indication, à cause d'une faute de frappe indiscernable,
Je le répète, il faut effectivement un minimum d'habitude pour comprendre certaines erreurs, mais, je gagne au final beaucoup plus de temps à apporter une solution aux problèmes que mon compilateur me signale qu'à attendre qu'un test unitaire me fasse, justement, comprendre que je me suis planté quelque part :aie:Si ce n'est, encore une fois (je crois que j'en ai déjà parlé dans cette discussion... ou dans l'une de celles que j'ai citées :aie:) que, malgré la compétence de ceux qui mettent les test unitaires au point et la qualité de ceux-ci, tout ce qu'ils prouvent, c'est que l'on n'a pas été en mesure de prendre les parties testées en faute, absolument pas que les parties testées sont exemptes d'erreur :aie:Citation:
on en vient à préférer parfois les erreurs à l'exécution, erreurs que les tests unitaires détectent de façon méthodique et fiable.
Ne crois pas que personne n'ait d'expérience dans d'autres langages que C++, c'est loin d'être vrai.Citation:
Un peu d'expérience dans d'autres langages ne fait pas de tort non plus. En Python, par exemple, il n'existe pas de "const". Il n'y a pas de "référence qui ne peut pas être nulle". Et pourtant, des systèmes complexes et très fiables sont développés en Python, bien plus rapidement qu'en C++ d'ailleurs. Après avoir écrit un programme de taille moyenne en Python, on revient en C++ et on se dit "mais pourquoi je me donne tout ce mal?"
Ce qui se passe, c'est que tout le monde est d'accord sur un point: C++ est autrement plus complexe, permet beaucoup plus facilement de se "tirer une balle dans le pied" que de nombreux autres langages.
C'est la raison pour laquelle on préférera permettre au compilateur de vérifier le plus de choses possibles, car quand on résout les problèmes qu'il met en avant, on s'évite de nombreuses erreurs.
Et tu as bien tord... Mais bon, je ne vais pas me répéter ;)Citation:
Et donc, je n'accepte pas comme idée reçue que "tout ce que le compilateur peut vérifier, on le prend". Malheureusement les programmeurs C++ prennent trop souvent le C comme point de comparaison: "mieux que le C", ce n'est pas encore si formidable que ça.
Le fait est que la fonction at n'est jamais qu'une version sécurisante (dans le sens où elle constate le dépassement d'index) de l'opérateur [].Citation:
Je suis bien d'accord qu'on ne doit pas camoufler un bogue par une exception. Or, un programme n'interagit pas qu'avec lui-même, mais aussi avec le monde extérieur, qu'il ne contrôle pas. Et là où les hypothèses qu'il fait sur le monde extérieur échouent, il rencontre une situation d'exception et se voit incapable de respecter sa propre spécification, en quel cas il devrait rapporter la situation aux instances supérieures (le client) par le moyen d'une exception.
Pour prendre la STL comme exemple encore une fois, la fonction vector::at(int index) retourne une référence sur l'objet situé à index, et lance une exception sinon. Donc, elle garantit la validité de la référence à l'aide d'une exception. Je ne vois pas comment la STL pourrait éviter de lancer une exception par "construction" étant donné que l'entrée ici (l'index) vient du monde extérieur (l'utilisateur) et est par conséquent incontrôlable. Si la STL ne lançait pas d'exception, elle serait contrainte de retourner soit une référence invalide (affreux), ou un pointeur nul (un peu mieux), ou un code d'erreur (...) mais le mieux est de lancer une exception.
Comme un vecteur peut contenir strictement n'importe quoi (pour autant qu'il soit copiable), on ne pouvait ni renvoyer un itérateur (car at ne renvoie pas un itérateur) ni NULL (car at ne renvoie pas de pointeur) ni quoi que ce soit de "plus ou moins commun".
Elle pourrait utiliser une assertion, mais elle ne serait donc sécurisée que... en mode débug :aie:
La seule solution qui reste, afin d'assurer cette sécurisation, c'est l'exception :)
pour la guerre de religion reference/pointeur j'ai dit plus haut que:
* pointeur c'est plutot pour les entités
* référence c'est plutot pour les valeurs
je n'ai vu personne commenter mais je me demande quand même ce que vous en pensez.
En y réfléchissant, ce que tu dit pourrais être pratique pour la compréhension du code.
Mais j'ai l'impression que cela reviens à dire qu'une entité est forcement alloué dynamiquement. Ce qui me gêne un peu.
Perso, j'ai l'impression que les références sont mieux comprise que les pointeurs. Ou plutôt mieux utilisé. Je trouve que dès qu'il y as un pointeur y as 10000 cas possible pour qu'il soit invalide. Alors qu'un référence y en as 100.
[edit]
Remarque en Qt
les QObject (classes entités) sont utilisés sous forme de pointeur
les classes valeurs (QString, QVector,...) sont utilisés en copy et const référence. Ou en pointeur quand c'est une sortie.
Disons que dans la plupart des cas cette définition me convient.
J'essaye de me tenir a ce qui suit:
* une valeur est definit avec le mot clé struct, et ne contient pas d'entité (mais eventellement un pointeur sur une entité), donc n'est qu'une aggrégation de valeurs
* une valeur devrait redéfinir les opérateurs ==, = ainsi qu'un constructeur de copie et (souvent) un operateur <. En tous cas, ceux-ci ont un sens même s'ils n'existent pas.
* les valeurs sont passés par copie ou par référence pour éviter la copie
* une entité est définie avec le mot-clé class
* une entité pourrait contenir une autre entité, bien que dans la plupart des cas il s'agit de pointeurs sur des entités
* une entité peut contenir des valeurs
* les opérateurs == ou < n'ont pas vraiment de sens
* les entités sont manipulées en general via des pointeurs
En prenant ces définitions j'ai du mal a voir dans quel cas (non tordus) une entité peut ne pas être allouée dynamiquement. Disons que c'est plus rare d'utiliser une entité "localement", c'est plutot le travail d'une valeur
Dans le cas plus haut, ce qui me choque dans l'exemple de dédé c'est ce code:
ou MyObject est une entité.Code:void DoSomething(const MyObject& o) { o.SpeakUp(); ... }
Ce code a peu de sens, et j'ai cherché a savoir ce qui me chiffone. Le passage d'une référence constante sur cet objet n'a pas vraiment de sens...
Or, MyObject est une entité (l'opérateur d'affectation n'a pas vraiment de sens, et il est impossible de définir logiquement un opérateur ==, et pas de copie possible). En effet, MyObject est juste une aggrégation de comportements.
Ce qui semble naturel pour moi c'est d'ecrire
la je vois parfaitement ce que ca veut dire et l'utilisation d'un pointeur (bien qu'il soit nu, et ca je ne cautionne pas forcément) résout le problème de l'auteur.Code:void DoSomething(MyObject* o) { o->SpeakUp(); ... }
Et ca ne me choque pas que toute les entités d'un programme soient allouées dynamiquement, ce qui me choque c'est l'allocation dynamique de valeurs
Pour le 1-, il y a un problème je suis d'accord, mais il est bien plus profond que const comme Joel l'a signalé.
Pour le 2-, je partage l'avis de gbdivers. C'est déjà bien d'avoir une garantie entre moi et moi-même (mais aussi les autres dév). Si en plus le compilo vérifie les contrats pour nous, c'est magique! À moi ensuite de ne pas chercher à contourner les invariants et autres contraintes d'utilisation que je pense avoir identifiés.
Et si je vois en cours de route je vois que j'ai eu la main trop lourde, je passe du cas restreint (à hypothèses simplificatrices) au cas général.
Accessoirement, tu viens de nous dire que les types ne servent à rien car il y aura toujours un couillon pour les réinterpréter des int en double.
Pour le 3-, J'ai déjà re-constifier des codes qui ne l'étaient pas. Souvent, il s'agissait de codes qui ne m'appartenaient pas, vu que je mets les const dès le début.
Du temps perdu ? Oui et non, ce fut aussi l'occasion de rentrer dans un code développé par un autre.
EDIT: zut, j'ai validé le message trop tôt.
Concernant la maintenabilité, dans "char* Func(int*, float*);" je lis que les deux buffers en entrée vont être altérés (ben oui, ce ne sont pas des références qui sont prises, donc c'est tableau C en in/out). Or je ne veux pas que la fonction que j'appelle les modifient. Que fais-je ? Ben je leur passe une copie. Et je perds du temps au passage à aller fouiller code & doc pour m'assurer que je ne perds pas la responsabilité de mes tableaux.
Alors que "func(std::auto_ptr<int>, float& r, vector<double> cont& )" me dit que la fonction va assumer la responsabilité d'un pointeur sur un (seul) int, modifier un flottant (éventuellement lire sa valeur), et utiliser sans altérer un vecteur qui est reçu en paramètre.
Côté maintenance, c'est clair comme de l'eau de roche, nul besoin d'aller ouvrir la doc ou le code pour savoir ce que je dois dupliquer, ce que je peux libérer, etc.
Il arrive qu'en paramètre je puisse prendre un "T const*", mais malgré les ambiguïtés sémantiques possibles, je considère par défaut qu'il s'agit d'un tableaux d'éléments que je n'ai pas le droit de modifier, probablement des histoires trainantes d'interfaçage avec du C.
Il y a-t-il vraiment besoin d'écrireCitation:
Pour les références, j'ai suffisamment argumenté et illustré qu'elles n'apportent rien de plus que les pointeurs si ce n'est une certaine légèreté syntaxique. Je comprends l'argument que les références apportent une sécurité accrue, mais pouvez-vous illustrer vos propos?
pour comprendre pourquoi on prend systématiquement des références quand on a le choix ?Code:
1
2
3
4
5
6
7 void f(T * p) { assert(p && "erreur de prog: je ne sais pas travailler avec un pointeur nul, mon dev est fainéant et refuse de perdre du temps sur les cas généraux qui ne riment à rien"); } void g(T & v) { ... }
b- J'ai expérience de deux situations de ce genre :
- j'ai trop joué avec des templates et sans équipement pour en simplifier les messages d'erreur (static_assert, et autre logger à templates)
- j'ai regardé un code d'un autre disposant du genre d'erreur que je ne commet plus (inversion entre , et ; etc)
Et mes problèmes n'ont jamais été induits par des références.
------------
Une référence et/ou un const prouvent plus qu'un TU vu qu'il faut le faire exprès pour les contourner. Ils sont vérifiés à la compilation, et ils m'évitent de faire un test qui valide que ma fonction me modifie pas le paramètre que je lui passe (cas du const), ou une batterie d'autres tests qui vérifient que ma fonction sera toujours appelée avec un pointeur non nul.
Prends ta fonction g, remplace la référence par un pointeur, recompile, et il n'y a pas magiquement plus de chances que v soit nul. On peut très bien s'assurer qu'un pointeur est non nul par design, sans besoin d'assertions ou autres. Il suffit de brièvement consulter n'importe quel code Java, C#, Python, Ruby, VB, etc., pour se rendre compte que dans ces langages où tout est une référence potentiellement nulle, on ne passe pas davantage son temps à faire des assertions ou à vérifier les valeurs de retour. (La gestion de mémoire automatique ne règle que le problème des références invalides, pas des références nulles.) Et qui plus est, on n'a pas deux syntaxes pour représenter la même chose, et on ne se pose jamais la question de laquelle faudrait-il choisir.Citation:
Il y a-t-il vraiment besoin d'écrire
Code:
1
2
3
4
5
6
7 void f(T * p) { assert(p && "erreur de prog: je ne sais pas travailler avec un pointeur nul, mon dev est fainéant et refuse de perdre du temps sur les cas généraux qui ne riment à rien"); } void g(T & v) { ... }
De toute manière, le vrai problème ce sont les pointeurs et références invalides ("dangling"); la cause d'un pointeur nul est relativement simple à identifier, celle d'un pointeur ou d'une référence invalide, moins. Et à ce niveau le fait d'utiliser une référence n'offre aucune garantie particulière.
Je ne nie pas le fait qu'il est parfois pratique d'utiliser une référence, comme dans ton exemple. Idem pour const, j'y reviendrai. Mais de façon générale, C++ est un langage inondé de fonctionnalités qui ne sont applicables que dans une minorité de cas, et quoiqu'elles puissent être utiles dans ces cas, le coût en complexité n'en vaut pas la peine d'après moi et mon expérience avec des langages plus simples. Je préfère un C++ quelque peu réduit et c'est comme ça que moi et mon équipe sommes les plus productifs. As-tu déjà essayé de faire un projet de taille moyenne sans le moindre const? Quand tu n'as même plus à te poser la question, c'est tout cette énergie que tu peux investir sur "résoudre le problème" plutôt que "se conformer au langage".
Concernant const, je vais essayer d'être parfaitement clair. C++, comme tous les langages impératifs, est un langage où tout est mutable par défaut. Le principe d'un langage impératif est en effet de modifier l'état du programme de façon séquentielle. Or les langages impératifs offrent souvent une façon de garantir qu'un membre de donnée ne peut être accidentellement réassigné ("final" en Java, "readonly" en C#, etc.) En C++, const va beaucoup plus loin. Étant donné que const se propage à tout ce qu'il touche, on se retrouve avec un langage où tout doit être const par défaut, explicitement, et ce qui ne l'est pas doit même parfois être déclaré mutable. Comme tout est mutable par défaut en C++, il en revient au programmeur de renverser la vapeur en saupoudrant des const partout où il est possible d'en mettre. Et ainsi, const devient omniprésent. Chaque méthode non-const a son équivalent const, et on n'en finit plus de dupliquer les signatures. La question d'immutabilité se pose au plus fin niveau de granularité (chaque méthode, chaque paramètre, chaque niveau d'indirection d'un pointeur même), à chaque instant, alors qu'il est rare que l'immutabilité soit si importante. Quel temps perdu!
En d'autres termes, si const pouvait être utilisé au besoin, je ne dirais pas non. Mais étant donné que c'est tout ou rien, je préfère rien. C'est clairement une fonctionnalité intrusive et qui interagit mal avec les templates comme j'ai illustré, donc mal pensée. C'est la faute de la spec des templates, oui, reste que ça brise const.
Tu dis que tu as déjà eu à rectifier du code qui n'était pas const. Mais que fais-tu si tu ne peux pas le modifier? Si TypeA n'offre pas de méthode const, et que je ne peux modifier le code source de TypeA, eh bien zut je ne peux pas m'en servir dans les méthodes const de mon type. Que fais-je? Je créé des équivalents non-const. Et ainsi je me retrouve avec des signatures dupliquées (si ce n'est des implémentations dupliquées, dans le cas de templates). Maintenance : -1.
Et oui, const sur un paramètre m'informe, sans avoir à lire la documentation, que tel paramètre ne sera pas modifié. Il m'informe aussi que si la fonction doit utiliser une certaine méthode de mon type, cette méthode devra être offerte en version const, ce que je ne peux savoir qu'en regardant son code source ou en tentant de compiler. Une simple description, qu'un bon IDE devrait faire apparaître sans même un clic, aurait eu le même effet sans imposer de contraintes sur mon code. Maintenance : -1.
De plus, tu supposes qu'un paramètre pris par référence non-const sera modifié. Or, c'est ambigu en fait. Peut-être est-ce un simple oubli de la part de celui qui a écrit la fonction. Il vaudrait mieux lire la description de la fonction que d'essayer de deviner l'intention de son auteur. Maintenance : -1.
Les types sont effectivement une chimère en C++ puisque ce avec quoi on interagit réellement, c'est de la mémoire brute. Or, reste que les types sont toujours utiles et toujours applicables. Ils ne garantissent rien, mais ils permettent au moins de diviser logiquement le programme, ce dont on aurait de la difficulté à se passer. Donc, chimère oui, mais chimère utile. const, quant à lui, ne garantit rien et ne fait que se répandre comme la peste.Citation:
Accessoirement, tu viens de nous dire que les types ne servent à rien car il y aura toujours un couillon pour les réinterpréter des int en double.
Il est vrai que le c++ à un problème dès qu'on touche aux template :
La plupart des mots clés (const notamment) obligent de créer une autre version de la même fonction. Pour cela, il suffit de lire le header functional du standard (si on regarde celui de c++0x, on obtient en plus les && pour dire rvalue).
Horrible tous ces templates alors que le code est le même !
Le const est très contreproductif. Cependant, il y a obligation de le mettre (ou mettre des const_cast partout).
Cependant, const a de nombreux avantages : il prévient la mauvaise utilisation : Combien de fois aurions nous vus "J'ai un problème avec std::string car il prend une mauvaise valeur après avoir appelé c_str() et avoir assigné la valeur".
Toutefois, je pense que cette discussion ne devrais pas avoir lieu dans "Pointeur intelligent vs pointeurs bruts" (Une scission de la discussion ?)
[EDIT]
J'aurais tendance à être d'accord avec Dr Dédé quand il dit que les références ne permettent que des raccourcis syntaxiques : toute référence est remplaçable par un pointeur non null.
La référence à le même défaut que le pointeur : elle peut être invalide.
Si une fonction prend un pointeur, il suffit de dire en précondition : "le pointeur ne peut être null" et, à partir de ce point, soit on considère qu'il faut quand même faire un assert (pour ceux n'ayant pas lu) ou alors considérer qu'il n'est pas null.
Mais la aussi, c'est une autre discussion : il ne s'agit pas de pointeurs intelligents et de pointeurs bruts...
Pour en revenir au sujet, les pointeurs intelligents sont utiles car ils permettent d'éviter la fuite mémoire, même en cas d'exception (ce qui j'aurais tendance à dire que ce n'est pas grave puisque une exception est sensée être extrêmement rare...).
@NoIdea: A part qu'un pointeur peut-etre NULL alors qu'une référence non. Et si en effet une référence peut-etre invalide au même titre qu'un pointeur, ces cas sont souvent des erreur de programmation (comprendre qui n'existe pas si le programme est bien fait), alors qu'un pointeur NULL n'est en aucun cas un problème de programmation et à un vraie sens, et doit donc être traité. (sauf si le contrfat dit l'inverse, mais dans ce cas pourquoi prendre un pointeur ? Ca sera juste plus verbeux à écrire et difficile à lire.)
@yan: Je ne comprend pas ton étonnement suite à l'édite de NoIdea, l'existence des deux versions pour chaque constante est primordiale, donc ce n'est pas inutile, et c'est bien une duplication de code (qu'on peut de temps en temps éviter, en jouant avec des cast par exemple, cf Eff++ item sur l'opérateur d'affectation je crois).
@Dr Dédé:
Tout les exemple que tu cites pour les const et les types sont vraie, mais ils supposent quand même un code de départ en désaccord avec ce qu'on dit. (même si ils fonctionnent :) )
Tu dis que tout est mutables par defaut, et je veus bien te croire, mais si j'ai un objet (d'un type quelconque), et que je lui envoyes le message "afficher" il est clair que je ne m'attends pas à ce que l'objet soit modifié, c'est en partie ceci que garantie const : que l'utilisateur ne modifie pas d'objets à son insue (sa suppose que le code soit totalement const-correct).
Pour tes exemple 1 et 3 (code de départ non const-correct et l'oublie), mélanger deux facon de coder conduit rarement à de bon résultat, et pour l'oublie : quand on fait une erreur c'est rare que ca marche bien ...
Le C++ est quand même typé, on peut en effet réinterpréter les données, mais c'est aux risques et périls de celui qui le fait, donc à moins de "se tirer une balle dans le pied" (copyright koala) je ne crois pas que ce soit un réel problème. (Ou alors le problème est que le C++ permet ce genre de chose, mais c'est une tout autre discussion AMA).
J'ai déplacé les messages sur le mot clef const sur
http://www.developpez.net/forums/d99...ts-clef-const/
yan