Programmation et valeur de retour II

Ce billet fait suite à Programmation et valeur de retour, un précédent billet dans lequel je présente les problèmes que posent certains langages dans la gestion des valeurs de retour.

J'ai décidé d'écrire une suite à ce billet car mon expérience s'est depuis enrichie d'un nouveau langage me permettant d'apporter un nouvel argument à mon idée : Visual Basic.

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.

Haut de page