Par valeur ou par référence : l’ambiguïté du passage de paramètres

Guillaume COCAULT
4 min readDec 8, 2018

--

En programmation impérative, il est possible de créer ses propres routines. Il s’agit de blocs de code destinés à réaliser une opération précise, pouvant prendre des paramètres, et capables de retourner des valeurs. Cependant, lorsqu’une routine modifie un paramètre, on constate parfois des comportements différents d’un langage à l’autre. Essayons de comprendre comment fonctionne le passage de paramètre à une procédure.

Passage de paramètre par valeur

Le passage de paramètre par valeur, ou call by value, consiste à copier la valeur du paramètre dans une variable locale, accessible seulement dans la portée de la fonction.

Le call by value travaille ainsi sur une copie locale de la variable qui est passée en paramètre. Sa portée est limitée à celle de la procédure, comme n’importe quelle autre variable. A priori, il n’est donc pas possible de récupérer le résultat de l’exécution de cette fonction, hormis en utilisant explicitement le mot clé return.

Call by value et type du paramètre

Considérons ces deux procédures, en JavaScript. La première incrémente un nombre passé en paramètre, et la deuxième incrémente le premier élément d’un tableau, donné lui aussi en paramètre.

function incrementeNb(a) {
a++
console.log("Dans la fonction, A=" + a)
}
function incrementeTab(t) {
t[0]++
console.log("Dans la fonction, t[0]=" + t[0])
}
n = 0
tab = [0]
incrementeNb(n)
incrementeTab(tab)
console.log("A l'extérieur de la fonction, A=" + n)
console.log("A l'extérieur de la fonction, tab[0]=" + tab[0])

>>> Dans la fonction, A=1
>>> Dans la fonction, t[0]=1
>>> A l'extérieur de la fonction, A=0
>>> A l'extérieur de la fonction, tab[0]=1

On constate une différence lors de l’exécution. Les fonctions ont bien incrémenté le paramètre, mais on remarque que la valeur initiale n’a pas été modifiée dans le premier cas, alors qu’elle l’a été dans le deuxième cas. Un résultat d’autant plus surprenant que la fonction ne retourne explicitement aucune valeur dans les deux cas…

Bien qu’ils paraissent très similaires, les deux paramètres sont en réalité extrêmement différents. En effet, dans le premier cas, le paramètre est un entier, alors que dans le deuxième cas, il s’agit d’un tableau d’entiers. Or, un entier est un type primitif, tandis que les tableaux sont des types non primitifs. Les types non primitifs étant a priori plus complexes et lourds en mémoire que les types primitifs, ce n’est pas une copie de leur valeur qui est transmise en paramètre des fonctions, mais un pointeur mémoire vers la valeur. Dans le cas d’un tableau, la complexité en temps et en mémoire pour la recopie est O(N). Le mécanisme de pointeur permet donc d’économiser du temps et de l’espace, surtout si les objets sont de grande taille.

Ainsi, dans l’exemple précédent, la valeur de l’entier a été donnée en paramètre de la première fonction, tandis qu’un pointeur vers le tableau d’entier a été donnée en paramètre de la deuxième fonction. Dans le premier cas, la fonction a modifié la valeur de l’entier, mais la modification a été perdue car il s’agissait d’une copie de l’entier initial. Dans le second cas, en revanche, la modification concerne le tableau pointé et non le pointeur. Elle reste donc effective en dehors de la portée de la fonction.

Call by value ou call by reference ?

En réalité, il existe plusieurs stratégies d’évaluation pour les paramètres de fonctions. Le call by value est l’une d’entre elles. Une seconde stratégie fréquente est le call by reference.

Le passage de paramètre par référence, ou call by reference, consiste à donner à la fonction un alias vers l’objet. La fonction utilise alors directement l’objet initial.

Avec l’exemple précédent, il n’est pas possible de savoir si l’évaluation se fait par valeur, avec éventuellement des pointeurs comme valeur, ou si elle se fait par référence. Il est toutefois possible de déterminer la stratégie d’évaluation utilisée par un langage, en implémentant une procédure swap, qui permute les valeurs de deux paramètres. Voici un exemple d’implémentation en JavaScript :

function swap(a, b) {
tmp = a
a= b
b= tmp
console.log("Dans swap : A=" + a[0] +" ; B=" + b[0])
}
a = [1]
b = [2]
console.log("Avant swap : A=" + a[0] +" ; B=" + b[0])
swap(a, b)
console.log("Apres swap : A=" + a[0] +" ; B=" + b[0])
>>> Avant swap : A=1 ; B=2
>>> Dans swap : A=2 ; B=1
>>> Apres swap : A=1 ; B=2

On passe deux tableaux en paramètre. On a vu qu’il s’agit en réalité de pointeurs vers des tableaux. Après avoir échangé leurs valeurs, on observe, dans la fonction, que le pointeur A fait référence au tableau B, et inversement. En revanche, en dehors de la fonction, les pointeurs A et B font toujours référence aux tableaux A et B respectivement. La modification n’est donc visible que dans la portée de swap. Ceci démontre que les pointeurs utilisés dans la procédure sont des copies des pointeurs initiaux, qui, eux, n’ont pas été modifiés.

On peut déduire de cet exemple que JavaScript utilise le call by value, tout comme Java, C ou C# par exemple. En C++ ou en Pascal, il existe une syntaxe pour demander explicitement une évaluation par référence. Dans ce cas, la fonction swap modifie durablement les pointeurs initiaux.

Conclusion

Ainsi, tous les langages de programmation n’utilisent pas la même stratégie pour l’évaluation des paramètres. Le C ou le Java utilisent le call by value, qui consiste à copier la valeur en paramètre. Le Pascal privilégie le call by reference, où un alias vers l’objet est donné en paramètre. Certains langages, comme le C++, proposent une combinaison de call by value et de call by reference.

Il ne faut pas confondre passage de paramètre par valeur, dont la valeur peut être un pointeur, et passage de paramètre par référence. Ces deux stratégies d’appel peuvent être distinguées en implémentant une fonction swap.

Enfin, il existe d’autre stratégies d’évaluation, comme le call by need, utilisé par Haskell, qui n’évalue les paramètres que lorsqu’ils sont utilisés.

--

--