De plus en plus de développeurs C s'éloignent du standard C89 avec entre autres l'avènement de C11. Il serait en effet facile d'imaginer qu'au minimum C99 soit raisonnablement bien répandu 15 ans plus tard. Mais c'est sans compter Microsoft Visual C++.
Microsoft ne supporte pas C99 avec MSVC. Même au niveau de la bibliothèque standard des fonctions qui leur serait pourtant assez facile de fournir n'existent pas, et leur équivalent n'est pas exactement équivalent.
Prenons l'exemple amusant (ou pas) de snprintf()
. Cette variante plus sûre de sprintf()
a en effet été standardisée dans C99. De son côté, MSVC fournit différentes variantes de fonctions qui ressemblent, dont _snprintf()
. Elles ressemblent suffisamment pour qu'un développeur naïf ajoute quelque chose comme #define snprintf _snprintf
pour compiler avec MSVC, mais ces fonctions ont de subtile différences qui peuvent se révéler ennuyeuses, voire pire, dangereuses.
Par exemple, C99 dit que snprintf()
retourne le nombre d'octets qui auraient été écrits si le buffer avait été suffisamment grand. Une astuce pratique qui repose sur cette propriété (via la variante vsnprintf()
) est de calculer la taille nécessaire pour un buffer avec vsnprintf(NULL, 0, fmt, ap)
¹ pour l'allouer ensuite. Mais _snprintf()
de MSVC retourne -1
si le buffer est trop petit.
C99 dit aussi que snprintf()
écrit toujours le \0
final si la taille du buffer est > 0, même si la sortie est tronquée par ce que le buffer est trop petit. Mais _snprintf()
de MSVC n'écrit le \0
final que si la sortie n'a pas été tronquée. Cette différence est dangereuse pour un habitué du comportement de C99, car il est alors courant de ne pas vérifier la valeur de retour de snprintf()
: même si ce n'est pas parfait, dans la pratique si l'appel est correct (format valide et arguments appropriés) snprintf()
réussira toujours. Au pire la sortie sera tronquée, mais rien de plus grave. Avec le comportement de _snpritnf()
par contre si la sortie est tronquée, utiliser le buffer peut être dangereux puisque il n'a pas de \0
final, et donc les fonctions de manipulation de chaînes ne qui ne prennent pas d'argument de taille vont lire au delà de la fin du buffer.
Tout ceci rend l'utilisation de snprintf()
assez difficile si l'ont veut supporter MSVC. Il y a bien sûr différentes solutions. La plus évidente est de ne pas dépendre du comportement de C99 et de toujours vérifier la valeur de retour. Si tous les appels vérifient que la valeur de retour n'est pas négative, et même si le comportement sera différent avec MSVC si la sortie est tronquée, il n'y a pas de risques puisque _snprintf()
retourne -1
lorsque la sortie est tronquée. Cependant cette solution peut impliquer beaucoup de petits changements ennuyeux, et a un comportement différent sous MSVC. De plus, il y a fort à parier que si snprintf()
a été utilisé dans le passé il le sera encore dans le futur, et il faudra donc toujours penser au comportement différent sous MSVC.
Une autre solution évidente, bien que plus compliquée, est d'écrire un wrapper pour fournir une version de snprintf()
qui se comporte comme C99 le demande. Avec _vsnprintf_s(buf, count, _TRUNCATE, fmt, ap)
et _vscprintf()
il reste assez simple d'écrire ce wrapper, même s'il faut faire attention aux détails de ces API (par exemple _vsnprintf_s()
n'accepte pas un count de 0).
Enfin bref, tout ça pour dire qu'il semble que C89 a encore de beaux jours devant lui, tout comme les bibliothèques qui fournissent une compatibilité C99 (et plus si affinité).
¹ Pour être complètement honnête, les implémentations pre-C99 de snprintf()
ont pour la plupart de subtiles différences, comme par exemple SUSv2 qui n'autorise pas count à être 0. C'est pour ça que l'ont voit souvent une variante avec un buffer de taille 1 :
char dummy;
int len = vsnprintf(&dummy, 1, fmt, ap);