Musée des horreurs – Bizarreries de langage

Quelques raccourcis :

Tout comme les langages de programmation ont leurs propres idiomes, ils offrent aussi leurs propres opportunités d'horreurs...

Entrée 00 – Quand le prototype et la définition ne concordent pas

La situation : soit le programme C++ suivant.

Horreur 00.0
#include <iostream>
float f(const float);
int main()
{
   std::cout << f(3);
}
float f(float x)
{
   x ++;
   return x;
}

L'horreur : le prototype déclare une fonction nommée f() et prenant en paramètre un float constant. La définition est celle d'une fonction nommée f() et prenant en paramètre un float non constant.

Contrairement à l'intuition, ce programme compile et s'exécute normalement (au sens où la fonction f() du programme est véritablement invoquée par le programme principal). Il ne s'agit pas d'un bug local à un compilateur ou à un autre mais bien d'un comportement normal.

Ce constat reste valide pour les objets (disons std::string) comme pour les types primitifs.

L'explication

Le langage C++ propose un modèle de compilation séparée. Chaque fichier source est compilé individuellement puis l'édition des liens entre en scène et connecte les invocations de sous-programmes au sous-programmes invoqués.

Le rôle du compilateur dans ce cas est de valider que l'invocation d'un sous-programme soit conforme à ce que le compilateur lui-même sait du sous-programme en question au moment de l'invocation, puis de générer du code correct pour que le lien entre invocation et invoqué puisse être tissé par l'éditeur de liens.

L'appel f(3) est correct au sens du prototype. Le passage de l'entier se faisant par valeur, le code généré pour le point dans main() où la fonction f() est invoquée demande de générer une copie de 3 dans un int puis d'appeler la fonction f().

Dans un autre ordre d'idées, la fonction f() telle que définie reçoit effectivement un int par valeur. Elle ne spécifie pas explicitement que ce int ne sera pas modifié (et, dans les faits, le modifie!). Cela ne pose pas de réel problème, cela dit, puisque le int est une copie de l'original.

La spécification const apposée à un paramètre par valeur dans la définition d'un sous-programme force le compilateur à vérifier que le paramètre en question n'est pas modifié dans la fonction. Le compilateur réalise donc une validation locale au sous-programme appelé mais pas une validation de correspondance entre la constance du paramètre dans le prototype et dans la définition.

En fait, si nous avions écrit le programme suivant...

Horreur 00.1
#include <iostream>
float f(const float);
int main()
{
   std::cout << f(3);
}

float f(float x)
{
   x++;
   return x;
}
float f(const float x)
{
   return x + 2;
}

... alors le compilateur nous aurait indiqué la présence de deux définitions pour la même fonction.

L'éditeur de liens, quant à lui, ne voit pas l'idée de constance ou non pour un paramètre passé par valeur, et ne fait que connecter l'invocation à l'invoqué.

Si nous avions utilisé un paramètre par valeur ou par adresse, toutefois, le compilateur aurait poussé plus loin la génération d'information quant aux types en jeu (l'enjeu est plus grand, après tout, si le paramètre effectif n'est pas une copie locale à la fonction invoquée) et l'éditeur de liens aurait remarqué l'incohérence entre le prototype et la définition.

Ainsi, ceci...

Horreur 00.2
#include <iostream>
float f(const float&);
int main()
{
   std::cout << f(3);
}
float f(float &x)
{
   x++;
   return x;
}

... générera une erreur à l'édition des liens.


Valid XHTML 1.0 Transitional

CSS Valide !