Illustration du problème en C++
Reprenons tout d'abord la fonction puissance codé originellement en C++
int pow(int nValue, int nPower) { int nReturn = 1; while( nPower > 0 ) { nReturn *= nValue; nPower -= 1; } return nReturn; }
Ma critique, il est vrai mal formulée dans mon premier article, vient des toutes premières lignes de cette fonction. En effet, bien que déclarant retourner un int
, demandant donc au compilateur d'allouer de l'espace pour une valeur de retour, nous sommes tout de même obligé de déclarer un nouvel int
dès la première ligne de notre fonction.
Ceci vient simplement du fait que l'espace réservé par le compilateur pour la variable de retour n'est accessible que par le mot-clé return
permettant d'effectuer la copie d'une variable dans la valeur de retour. Le problème vient donc uniquement du fait que le programmeur ne possède aucun moyen d'accès sophistiqué à cet espace mémoire en C++.
La solution en Visual Basic
Dans Visual Basic au contraire, le design de ce langage permet au programmeurs de manipuler cet espace. Voici ce que donnerait la fonction puissance en Visual Basic 6.
Function Pow(ByVal nValue As Long, ByVal nPower As Long) Pow = 1 Do While nPower > 0 Pow = Pow * nValue Loop End Function
On remarque tout de suite qu'il n'est plus nécessaire de déclarer une nouvelle variable pour retourner le résultat, le langage nous laissant utiliser le nom de la fonction pour accéder à la variable de retour. Voilà une astuce intéressante.
Avantages
Le premier avantage est qu'il est n'est pas nécessaire pour le programmeur d'utiliser une nouvelle variable. Le but d'une fonction est sémantiquement de créer une nouvelle variable, le langage nous fournit donc logiquement un moyen d'accéder directement à cette nouvelle variable durant son processus de création.
Le second avantage est la séparation forte entre la notion de valeur de retour et la notion de fin de la fonction. En C++, le mot-clé return
a non seulement l'effet de copier une valeur dans la valeur de retour mais aussi de quitter sur le champs l'exécution de la fonction.
Or il est souvent nécessaire d'effectuer des traitements après avoir calculer la valeur de retour, en C++, cela n'est faisable qu'en utilisant une variable locale. Une illustration que je fais souvent de ce problème est la différence en C++ entre l'opérateur de i++
et ++i
. Regardons l'implémentation de ces deux fonctions en C++ :
int PreIncrement(int& i) { i = i + 1; return i; } int PostIncrement(int& i) { const int nSave = i; i = i + 1; return nSave; }
On voit très bien que le PostIncrement
devant retourner la valeur de i
avant son incrémentation, on est obligé d'en sauvegarder la valeur dans une variable locale, consommant ainsi 4 précieux octets.
L'implémentation des mêmes fonctions en Visual Basic ne pose plus ce problème :
Function PreIncrement(ByRef i As Long) As Long i = i + 1 PreIncrement = i End Function Function PostIncrement(ByRef i As Long) As Long PostIncrement = i i = i + 1 End Function
En Visual Basic, il est possible d'utiliser la valeur de retour comme une variable locale. Ainsi, on peut fixer sa valeur, voir même effectuer des calculs avec. La fonction PostIncrement
n'utilise alors plus de variable locale, elle modifie juste la valeur de retour avant d'effectuer son traitement, ce qui est impossible en C++.
Discussion
Il est vrai que l'approche de Visual Basic n'est pas simple à appliquer à un langage comme le C++. Visual Basic initialisant toujours les variables, dans le cas d'un appel de fonction, la valeur de retour est donc toujours initialisée à la valeur par défaut pour le type de retour (0 pour les nombres).
Cette initialisation permet de s'assurer que même si le programmeur n'assume pas sa responsabilité de remplir la valeur de retour, son état est toujours correct. Ce genre d'approche n'est bien sûr pas du tout possible dans un langage comme C++ qui se veut bas niveau.
Le choix fait par C++ pour s'assurer de la validité des valeurs de retour est de toujours appeler le constructeur par recopie, cet appel étant fait par le mot-clé return
dont la présence est obligatoire dans les fonctions. Finalement, on se rend compte que cette solution est loin d'être meilleure que celle choisie par Visual Basic.
Cependant, il serait intéressant de fournir un accès plus fin à la variable de retour, pour tous les avantages ci-dessus. En obligeant le programmeur a effectuer un appel à un constructeur quelconque pour le type retourné, le langage serait ainsi assuré de la validité de la valeur de retour tout en laissant la liberté au programmeur de construire la valeur de retour comme il le souhaite.
Ainsi la fonction PostIncrement pourrait ressembler à cela :
int PostIncrement(int& i) { return = i; // Appel d'un constructeur par recopier de int avec la valeur i i = i + 1; }
Et pour reprendre mon exemple du précédent article avec les matrices, voici ce que j'aimerais faire dans ma méthode Multiply()
Matrix Matrix::Multiply(const Matrix& m) { return = Matrix(0); // Appel d'un constructeur spécial n'initialisant pas les données // Remplissage de la matrice Multiply ... }
La méthode Multiply()
retrouve alors son statut de constructeur de variable, avec toute la responsabilité que cela implique. Cette approche a aussi pour intérêt que les programmeurs ne se tiennent plus à l'écart des retours par valeur, souvent très coùteux mais bien plus clair syntaxiquement et sémantiquement.
Juste un mot sur GCC
Les nouvelles fonctionnalités que je présente ne sont bien sûr pas présentes dans le langage C++ et il est impossible de les utiliser avec votre compilateur habituel, cependant les compilateurs redoublent d'imagination pour contourner ce problème.
Le choix que fait GCC n'oblige pas le programmeur à utiliser un constructeur sur la valeur de retour. Il préfère partir du principe que si le programmeur retourne une variable locale (construite avec le constructeur de son choix), son intention est en fait de définir la valeur de retour.
Ainsi, lorsque le programmeur définit une variable locale qui sera utilisé avec le mot-clé return
, GCC modifie alors le code pour que le programmeur accède directement à la valeur de retour lorsqu'il utilisera cette variable locale, plutôt que de créer une nouvelle variable, faisant ainsi l'économie de la création d'une nouvelle variable.
1 De loufoque -
Ce n'est pas un choix de GCC, c'est une optimisation autorisée et même conseillée indiquée dans le standard. Et qui est d'ailleurs disponible dans tous les compilateurs modernes.
Dans ton précédent billet, tu avais fait le test avec MSVC6, qui n'implémente que partiellement cette optimisation (il fait la RVO mais pas la NRVO) mais qui est de toutes manières totalement obsolète. Le considérer comme un compilateur C++ est plus une blague qu'autre chose, tout ce qu'il compile c'est un langage qui y ressemble, et de toutes manières C++98 (le premier standard C++) n'était pas encore sorti à l'époque.
2 De Vincent -
Après quelques recherches, je vais dans ton sens loufoque, merci de ton commentaire. GCC implémente bien des fonctionnalités optionnelles précisées dans le standard.
Pour précision, le RVO est une optimisation (Return Value Optimization) permettant de se passer du constructeur par recopie lors du retour d'un nouvel objet non nommé. ex: return Object();
Le NRVO (Named Return Value Optimization) est simplement un niveau supplémentaire permettant d'activer l'optimisation dans le cas d'une variable nommée comme dans mes exemples.
En ce qui concerne VC6, je trouve qu'il ne s'en sort pas trop mal pour un compilateur de son âge censé compilé un langage pas encore standardisé.
Enfin, un axe de réflexion intéressant est de se demander si ces optimizations mises en place dans GCC et faisant bien partie du standard doivent être prises en compte par le développeur.
Concevoir son code pour qu'il respecte le standard C++ est logique, mais ce standard nous laisse le choix d'utiliser ou non les NRVO. Quelle est donc la meilleure démarche ? Oublier cette fonctionnalité et développer afin d'avoir de bonnes performances sur tous les compilateurs ? Ou en tenir compte pour clarifier le code et assumer le fait que les langages n'implémentant pas l'optimization compileront un programme plus lent ?
3 De loufoque -
Je conseille de se baser sur le fait que cela sera optimisé. Bien sûr ce n'est pas vraiment valable si tu fais un programme dont l'unique finalité est de tourner avec un compilateur qui ne fait pas cette optimisation. En particulier, si tu es bloqué avec MSVC6. (que l'on peut mettre à jour gratuitement, je le rappelle -- et on peut même utiliser l'IDE avec un compilateur plus récent)
De toutes façons dans la prochaine version du standard il y aura les sémantiques de mouvement, ce qui fait que si jamais ce n'est pas optimisé, l'objet ne sera jamais que déplacé, pas copié. (bien sûr dans les cas où l'objet est déplaçable) %% Déplacement qui, dans 99,99% des cas, se résume à un memcpy de la taille de l'objet. (je n'ai pas encore trouvé de cas où il était sémantiquement correct de faire autre chose)
4 De Mister Bark -
Bonjour ! ton article me rapelle de sacrés souvenirs de codage ! j'ai abandonné le VB pour le perl il y a bien longtemps mais ca me fait du bien de lire un peu ton code :)
Et pour ton exemple, envisages-tu de faire une petite équivalence en perl ? ;)
@ bientot !
5 De Vincent -
Pas du tout ! Je ne connais pas assez Perl pour ça :-)