Caster, c’est mal

En C comme en C++, on se retrouve de temps en temps à caster une variable (de l’anglais to casttranstyper en français). Pourtant, caster, c’est mal. Il faut le faire avec parcimonie et avoir de bonnes raisons quand on décide de le faire. Juste « faire taire un warning du compilateur » n’est pas une bonne raison. Quand on caste, on dit au compilateur : « considère cette variable comme étant d’un autre type, ne fais pas de vérification dessus ». Dans un code parfaitement écrit, on en devrait jamais avoir besoin de caster puisque les types seraient toujours bons. La réalité est un peu différente et il faut éviter de cacher les problèmes potentiellement graves.

Voici un premier exemple :

void print(float* p) {
    printf("%f", *p);
}

int main() {
    int i = 31415;
    print(&i);
}

Ce code génère un warning en C :

warning: passing argument 1 of 'print' from incompatible pointer type [-Wincompatible-pointer-types]
     print(&i);
           ^
main.c:3:6: note: expected 'float *' but argument is of type 'int *'
 void print(float* p) {
      ^~~~~

Il génère une erreur en C++ :

error: cannot convert 'int*' to 'float*' for argument '1' to 'void print(float*)'
     print(&i);

Dans les 2 cas, caster permet de faire taire le compilateur mais le résultat est bien sûr faux, ça affiche 0.000000. Le cast permet ici de considérer une adresse vers un type comme étant une adresse vers un autre type. On ne change pas la valeur de l’adresse et donc à l’emplacement mémoire, printf() trouve des bits qui correspondent à un entier de 32 bits (probablement codé en complément à 2) et les lit en considérant qu’ils représentent un flottant (logiquement codé en IEEE754). Il n’y a quasiment aucune chance que les 2 représentations donnent la même valeur et l’affichage est faux.

Le problème de ce premier code était assez évident. Prenons un second code un peu plus subtil :

void print(float p) {
	printf("%f", p);
}

int main() {
	int i = 31415;
	print(i);
}

Compilé avec les options -Wall -Wextra, ce code ne génère pas de warning. On pourrait se dire que tout va bien, les langages C et C++ autorisant les conversions entre nombres. Autorisées ne veut pas forcément dire parfaites. Ajoutons l’option -Wconversion pour voir ce que GCC a beau à nous dire :

warning: conversion to 'float' from 'int' may alter its value [-Wconversion]
  print(i);

Que vérifie exactement ce warning ? Voici sa documentation :

-Wconversion
Warn for implicit conversions that may alter a value. This includes conversions between real and integer, like abs (x) when x is double; conversions between signed and unsigned, like unsigned ui = -1; and conversions to smaller types, like sqrtf (M_PI). Do not warn for explicit casts like abs ((int) x) and ui = (unsigned) -1, or if the value is not changed by the conversion like in abs (2.0). Warnings about conversions between signed and unsigned integers can be disabled by using -Wno-sign-conversion.

For C++, also warn for confusing overload resolution for user-defined conversions; and conversions that never use a type conversion operator: conversions to void, the same type, a base class or a reference to them. Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.

Bien sûr, un cast empêche le warning d’apparaître. Warning ou pas, la sortie console est bien 31415.000000. En revanche, si i = 987654321, alors la sortie devient 987654336.000000… Et de manière marrante, si i = 987654336 alors la sortie est bien 987654336.000000 et est donc correcte. Vous êtes surpris qu’un code qui passe bien avec -Wall -Wextra puisse produire des résultats erronés ? Hé hé… Premièrement, ces deux options sont le minimum vital et vous pouvez (devez ?) en ajouter d’autres, par exemple -Wwrite-strings. Deuxièmement, les conversions entre nombres, que ce soit lors de simples affectations ou lors de calculs, réservent bien des surprises et vous devriez vous méfiez. -Wconversion est fait pour ça ; -Wsign-conversion et -Wfloat-conversion sont un peu moins brutaux (ils sont activés par -Wconversion automatiquement). Il existe aussi -Wdouble-promotion qui peut révéler des possibles pertes de performances.

Ici, ce n’est que de l’affichage, ce n’est pas catastrophique, mais si c’était des calculs pour la stabilisation votre drône DIY, vous rigoleriez moins ! ; )

Voici un dernier exemple bien plus violent :

void modify(char* p) {
	p[0] += 1;
}

int main() {
	char* message = (char*) "hello";
	modify(message);
}

Ce code cache un warning en C++ :

warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
  char* message = "hello";
                  ^~~~~~~

En C, il faut rajouter l’option explicitement car elle n’est pas inclus dans -Wall -Wextra. Que se passe t-il si on exécute ce code ? Une erreur de segmentation, tout simplement, puisque le programme va tenter d’écrire dans une zone mémoire qui est en lecture seule.

Cet article a prouvé au passage que le système de typage est plus fort en C++ qu’en C mais ce n’était pas son but premier. Le but était de vous montrer que le système de typage est là pour vous aider et que caster vous laisse seul face à vos erreurs au lieu de bénéficier de l’aide de votre compilateur. Rares sont les codes C et C++ sans cast, surtout si vous faites joujou avec du code très bas niveau. Essayez toujours de ne pas avoir à caster, utiliser les opérateur de cast en C++ pour expliciter le but du cast, méfiez-vous des conversions implicites et activer les options de votre compilateur pour qu’il vous aide à détecter autant d’erreurs que possibles.

2 Réponses

  1. JackDesBwa

    Je viens de recevoir une notification sur un ancien commentaire que j’avais fait sur ton blog et ça m’a fait atterrir sur cet article. Vaste question que le transtypage et je me demandais ce que tu allais raconter là-dessus…
    Rassure-toi, je ne vais pas te contredire cette fois ; juste quelques idées arrivant aléatoirement.

    Il faut noter deux types de transtypage : implicites et explicites. Dans le langage C (et C++), il a été choisi de faciliter le transtypage via des règles de conversion implicites parce que c’est pratique ; mais c’est aussi parfois piégeux. C++ est plus restrictif, notamment en ce qui concerne les conversion implicites qui peuvent changer la valeur, mais il faut rester vigilant néanmoins.
    Je me suis fait méchamment avoir il y a quelques années à cause du typage implicite des constantes qui faisait qu’un calcul intermédiaire se faisait dans une précision plus faible que je pensais et par transtypage implicite la compilation fonctionnait, mais le résultat était faux.

    Pour les transtypages explicites, il existe quatre types de conversions qui ont chacune un mot-clé en C++. La syntaxe C est proche du reinterpret_cast qui dit explicitement au compilateur que le programmeur prend la responsabilité de ne pas briser les contrats implicites et qu’il accepte sans concession que ça fasse n’importe-quoi s’il en oublie le moindre détail. Évidemment, c’est dans la plupart des cas une mauvaise pratique.

    J’encourage vivement à utiliser les types (et non les typedef) pour créer des types incompatibles qui permettent de sécuriser les API. C’est une possibilité qu’offre le C++ qui à elle seule en fait un langage bien supérieur au C.
    Par exemple sur un projet, j’avais des communications qui pouvaient se faire vers une carte1 et une carte2 avec des messages pouvant être 1, 2, 3 pour la carte1 et 2, 3 pour la carte2. Au lieu d’utiliser des entiers (ou des enums), j’ai créé des types (non transtypables) et une fonction send surchargée pour chaque combinaison de types [évidemment le contexte était plus complexe et la probabilité d’erreur grande, surtout pour mes collègues moins au fait du protocole que moi ; et oui, j’ai mixé ça avec des enums pour éviter les nombres magiques].
    Mais un jour je me suis fait avoir moi-même et le compilateur, voyant des types incompatibles, m’a indiqué une erreur dès la compilation et nous avons gagné de nombreuses heures de déverminage.
    En plus, ça ne coûte rien à l’exécution et dans certains cas, l’utilisation du typage peut permettre de gagner du temps à l’exécution. Par exemple, en ayant un type « sûr » pouvant être converti depuis un type « non sûr » si certaines vérifications passent, il est possible d’avoir une partie de l’API qui accepte seulement les types « sûrs » et qui n’a besoin de faire aucun test sur les données puisque le type a été codé pour assurer qu’elles vérifient les propriétés souhaitées (préconditions). Évidemment un transtypage sauvage tel que décrit dans l’article met à mal ceci, mais dans ce cas, le programmeur indique au compilateur qu’il se fiche complètement de ce que va faire le programme.

    Enfin, dernière petite touche à propos des outils. Activer les warnings des compilateurs ET avoir une politique à zéro warnings (sans transtyper sauvagement) est une bonne idée.
    Pour ma part, j’ai gagné quelques bonnes pratiques en suivant les conseils de l’outil clang-tidy (au passage, clang-format est un excellent outil également). D’ailleurs j’ai un IDE qui affiche les warnings de clan-tidy par dessus mon code et il est difficile de les ignorer, ce qui rend le code plus robuste avant même de l’avoir compilé.

    J'aime

    6 décembre 2018 à 6:47

    • Hey ! Ca fait plaisir de te lire !

      Tu utiliserais pas CLion à tout hasard ?

      C’est vrai qu’en C++, tu peux aller bien plus loin que le C sur les vérifications à compile-time, il faut carrément en profiter :p

      J'aime

      27 décembre 2018 à 11:37

Répondre

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l'aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.