Cet article a fait l'objet d'une traduction automatique.
Windows et C++
Utilisation de Printf avec le C++ moderne
Que faudrait-il pour moderniser le printf ? Cela peut sembler comme une question étrange à beaucoup de développeurs qui pensent que C++ fournit déjà un substitut moderne pour printf. Alors que le titre de gloire de la bibliothèque Standard du C++ est sans aucun doute l'excellent Standard Template Library (STL), il comprend également une fonction de flux entrée /bibliothèque de sortie qui ne ressemble en rien par rapport à la STL et incarne aucun de ses principes relatifs à l'efficacité.
« Programmation générique est une approche de la programmation qui met l'accent sur la conception des algorithmes et structures de données afin qu'ils travaillent dans le cadre plus général sans perte d'efficacité, » selon Alexander Stepanov et Daniel Rose, dans le livre, "De mathématiques à Generic Programming" (Addison-Wesley Professional, 2015).
Pour être honnête, printf ni le cout n'est aucunement représentatif de C++ modernes. La fonction printf est un exemple d'une fonction variadique et l'un de des bonnes utilisations de cette fonction un peu fragile, hérité de la programmation en langage C. Fonctions variadique datent d'avant variadic templates. Ces derniers offrent une installation vraiment moderne et robuste pour faire face avec un nombre variable d'arguments ou de types. En revanche, cout n'emploie variadique quoi que ce soit, mais au contraire s'appuie donc fortement sur les appels de fonction virtuelle que le compilateur ne peut pas faire grand-chose pour maximiser son rendement. En effet, l'évolution du design de la CPU a favorisé printf tout en faisant peu pour améliorer la performance de l'approche polymorphe de cout. Donc, si vous voulez la performance et l'efficacité, printf est un meilleur choix. Il produit également le code qui est plus concis. Voici un exemple :
#include <stdio.h>
int main()
{
printf("%f\n", 123.456);
}
Le spécificateur de conversion %f raconte printf pour s'attendre à un nombre à virgule flottante et convertissez-le en notation décimale pointée. Le \n est juste un caractère de saut de ligne ordinaire qui peut être élargi pour inclure un retour chariot, selon la destination. La conversion en virgule flottante suppose une précision de 6, concernant le nombre de chiffres qui apparaît après la virgule. Ainsi, cet exemple affichera les caractères suivants, suivies d'une nouvelle ligne :
123.456000
Atteindre le même objectif avec cout semble relativement droitevers l'avant dans un premier temps :
#include <iostream>
int main()
{
std::cout << 123.456 << std::endl;
}
Ici, cout s'appuie sur la surcharge des opérateurs pour diriger ou envoyer le nombre à virgule flottante dans le flux de sortie. Je n'aime pas l'abus de cette sorte de surcharge d'opérateur, mais j'avoue que c'est une question de style personnel. Enfin, endl conclut en insérant une nouvelle ligne dans le flux de sortie. Toutefois, cela n'est pas tout à fait identique à l'exemple de printf et produit un résultat avec une précision décimale différente :
123.456
Cela conduit à une question évidente : Comment puis-je changer la précision pour les abstractions respectifs ? Eh bien, si je veux seulement deux chiffres après la virgule, je peux simplement indiquer ceci dans le cadre du spécificateur printf flotteur-point conversion :
printf("%.2f\n", 123.456);
Maintenant printf arrondit le nombre pour produire le résultat suivant :
123.46
Pour obtenir le même effet avec cout nécessite un peu plus de taper :
#include <iomanip> // Needed for setprecision
std::cout << std::fixed << std::setprecision(2)
<< 123.456 << std::endl;
Même si vous ne me dérange pas le niveau de détail de tout cela et préfère profiter de la souplesse ou l'expressivité, n'oubliez pas que cette abstraction est livré à un coût. Tout d'abord, les manipulateurs fixes et setprecision sont avec États, ce qui signifie que leur effet persiste jusqu'à ce qu'ils sont inversés ou réinitialiser. En revanche, le spécificateur printf de conversion comprend tout le nécessaire pour que la conversion simple, sans affecter n'importe quel autre code. Les autres frais peuvent être pas important pour la plupart sortie, mais le jour pourrait venir où vous remarquez que celles des autres programmes peuvent produire autant de fois plus rapide que le vôtre peut. Mis à part la surcharge d'appels de fonction virtuelle, endl aussi vous donne plus que vous pourriez avoir négocié. Non seulement il envoie une nouvelle ligne à la sortie, mais elle provoque également le flux sous-jacent vider sa sortie. Lors de la rédaction de tout type d'e/s, que ce soit à la console, un fichier sur disque, une connexion réseau ou même un pipeline graphique, chasse d'eau est généralement très coûteuse et purges répétées seront sans aucun doute altérer les performances.
Maintenant que j'ai exploré et printf et cout en contraste un peu, il est temps de revenir à la question initiale : Que faudrait-il pour moderniser le printf ? Sûrement, avec l'avènement du C++ modernes, comme exemplifié par C ++ 11 et autres, je peux améliorer la productivité et la fiabilité de printf sans sacrifier les performances. Une autre un peu indépendants membres de la bibliothèque Standard du C++ est la classe de chaîne officielle de la langue. Bien que cette classe a également été décriée au fil des ans, il offre des performances excellentes. Bien que pas sans faille, il fournit un moyen très utile pour gérer les chaînes en C++. Par conséquent, toute modernisation de printf devrait vraiment jouer gentiment avec chaîne et wstring. Nous allons voir ce qui peut être fait. Tout d'abord, permettez-moi d'aborder ce que je considère être le problème plus contrariants de printf :
std::string value = "Hello";
printf("%s\n", value);
Cela devrait vraiment travailler, mais je suis sûr que vous pouvez le constater, au contraire, cela aboutira à ce que l'on appelle affectueusement « un comportement non défini. » Comme vous le savez, printf est tout au sujet de texte et de la classe de chaîne C++ est la première manifestation du texte en langage C++. Ce que je dois faire est d'envelopper le printf de telle manière que cela fonctionne, tout simplement. Je ne veux pas avoir à arracher à plusieurs reprises sur le tableau de la chaîne caractères terminée par null comme suit :
printf("%s\n", value.c_str());
C'est juste pénible, alors je vais y remédier en enroulant le printf. Traditionnellement, cela a impliqué d'écrire une autre fonction variadique. Peut-être quelque chose comme ceci :
void Print(char const * const format, ...)
{
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
Malheureusement, cela me gagne rien. Il pourrait être utile encapsuler des variantes de printf afin d'écrire dans certains autres tampons, mais dans ce cas, j'ai gagné rien de valeur. Je ne veux pas revenir à des fonctions de style C variadique. Au lieu de cela, je veux impatients et embrasser C++ modernes. Heureusement, grâce aux C ++ 11 variadic templates, je n'aurez jamais à écrire une autre fonction variadique dans ma vie. Plutôt que la fonction printf d'emballage dans une autre fonction variadique, je peux au lieu de cela l'envelopper dans un modèle variadique :
template <typename ... Args>
void Print(char const * const format,
Args const & ... args) noexcept
{
printf(format, args ...);
}
Dans un premier temps, il ne semblerait pas que j'ai gagné beaucoup. Si je devais appeler la fonction d'impression comme ceci :
Print("%d %d\n", 123, 456);
cela provoquerait les args pack de paramètre, constitués de 123 456, d'étendre à l'intérieur du corps du modèle variadique comme si j'avais simplement écrit ceci :
printf("%d %d\n", 123, 456);
Alors qu'ai j'ai gagné ? Bien sûr, je vous appelle printf plutôt que vprintf et je n'ai besoin gérer une va_list et la pile associés -tourner les macros, mais je suis toujours simplement forwarding arguments. Don' t oublier la simplicité de cette solution, cependant. Encore une fois, le compilateur va déballer arguments de la fonction template comme si j'avais simplement appelé printf directement, ce qui signifie qu'il n'y a pas de surcharge dans leur emballage printf de cette façon. Cela signifie aussi que c'est toujours première classe C++ et je peux utiliser des techniques de métaprogrammation puissant de la langue d'injecter tout code requis — et d'une manière complètement générique. Plutôt que de simplement élargir le pack du paramètre args, je peux envelopper chaque argument pour ajouter tout ajustement nécessaire par printf. Considérez ce modèle de fonction simple :
template <typename T>
T Argument(T value) noexcept
{
return value;
}
Il ne semble pas faire grand chose et en effet ce n'est pas, mais je peux maintenant étendre le pack de paramètre pour envelopper chaque argument dans l'une de ces fonctions comme suit :
template <typename ... Args>
void Print(char const * const format,
Args const & ... args) noexcept
{
printf(format, Argument(args) ...);
}
Je peux toujours appeler la fonction imprimer de la même manière :
Print("%d %d\n", 123, 456);
Mais il produit maintenant efficacement l'expansion suivante :
printf("%d %d\n", Argument(123), Argument(456));
C'est très intéressant. Bien sûr, il ne fait aucune différence pour ces arguments entiers, mais je peux maintenant surcharger la fonction Argument pour gérer les classes de chaîne C++ :
template <typename T>
T const * Argument(std::basic_string<T> const & value) noexcept
{
return value.c_str();
}
Ensuite, je peux simplement appeler la fonction imprimer avec certaines chaînes :
int main()
{
std::string const hello = "Hello";
std::wstring const world = L"World";
Print("%d %s %ls\n", 123, hello, world);
}
Le compilateur augmentera efficacement la fonction printf interne comme suit :
printf("%d %s %ls\n",
Argument(123), Argument(hello), Argument(world));
Cela garantit que tableau de chaque chaîne caractères terminée par null est fournie pour printf et provoque un comportement totalement bien défini :
123 Hello World
Ainsi que le modèle de fonction Print, j'utilise aussi un certain nombre de surcharges pour une sortie non formatée. Cela tend à être plus prudent et empêche que printf accidentellement une mauvaise interprétation des chaînes arbitraires comme contenant des spécificateurs de conversion. Figure 1 répertorie ces fonctions.
Figure 1 impression sortie non formaté
inline void Print(char const * const value) noexcept
{
Print("%s", value);
}
inline void Print(wchar_t const * const value) noexcept
{
Print("%ls", value);
}
template <typename T>
void Print(std::basic_string<T> const & value) noexcept
{
Print(value.c_str());
}
Les deux premières surcharges formater simplement ordinaires et caractères larges baies, respectivement. Le modèle final void transmet à la surcharge appropriée, selon la question de savoir si une chaîne ou wstring est fournie comme argument. Compte tenu de ces fonctions, je peux en toute sécurité imprimer plusieurs spécificateurs de conversion littéralement, comme suit :
Print("%d %s %ls\n");
Qui prend soin de mon reproche plus courante avec printf en résultat de manière transparente et en toute sécurité de la chaîne de manutention. Qu'en est-il de mise en forme des chaînes elles-mêmes ? La bibliothèque Standard de C++ fournit différentes variantes de printf pour écrire à tampon de chaîne de caractères. Parmi ceux-ci, je trouve snprintf et swprintf les plus efficaces. Ces deux fonctions gérer de caractère et la sortie de caractères étendus, respectivement. Ils vous permettent de spécifier le nombre maximal de caractères qui peuvent être écrits et retourner une valeur qui peut être utilisée pour calculer la quantité d'espace est nécessaire devrait l'original tampon ne soit ne pas assez grand. Toujours, sur leurs propres, ils sont sujettes à erreur et assez fastidieux à utiliser. Temps pour certains C++ modernes.
C ne supporte pas surcharge de fonctions, mais c'est beaucoup plus facile d'utiliser une surcharge en C++ et cela ouvre la porte pour la programmation générique, alors je vais commencer en encapsulant les snprintf et swprintf comme fonctions appelées StringPrint. J'utiliserai aussi des modèles de fonction variadique donc je peux profiter de l'expansion de l'argument sécuritaire que j'ai déjà utilisé pour la fonction d'impression. La figure 2 fournit le code pour les deux fonctions. Ces fonctions affirment également que le résultat n'est pas -1, qui est ce que les fonctions sous-jacentes retournent quand il ya un problème récupérable, l'analyse de la chaîne de format. J'utilise une assertion car j'imagine que c'est un bug et devrait être fixé avant expédition code de production. Vous pouvez remplacer ceci par une exception, mais gardez à l'esprit il n'y a aucun moyen de balles de transformer toutes les erreurs en exceptions, car il est toujours possible de passer des arguments non valides qui mèneront à un comportement non défini. C++ moderne n'est pas idiot-proof C++.
Figure 2 Low-fonctions mise en forme de chaîne de niveau
template <typename ... Args>
int StringPrint(char * const buffer,
size_t const bufferCount,
char const * const format,
Args const & ... args) noexcept
{
int const result = snprintf(buffer,
bufferCount,
format,
Argument(args) ...);
ASSERT(-1 != result);
return result;
}
template <typename ... Args>
int StringPrint(wchar_t * const buffer,
size_t const bufferCount,
wchar_t const * const format,
Args const & ... args) noexcept
{
int const result = swprintf(buffer,
bufferCount,
format,
Argument(args) ...);
ASSERT(-1 != result);
return result;
}
Les fonctions de StringPrint offrent un moyen générique de traiter avec la chaîne mise en forme. Maintenant je peux me concentrer sur les spécificités de la classe string, et il s'agit surtout de gestion de la mémoire. Je tiens à écrire du code comme ceci :
std::string result;
Format(result, "%d %s %ls", 123, hello, world);
ASSERT("123 Hello World" == result);
Il n'y a aucune gestion de la mémoire tampon visible. Je ne dois pas figurer dehors comment grand un tampon à allouer. Je demande simplement la fonction Format pour logiquement attribuer le résultat mis en forme à l'objet string. Comme d'habitude, le Format peut être un modèle de fonction, plus précisément un modèle de variadic :
template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
T const * const format,
Args const & ... args)
{
}
Il existe une variété de façons d'implémenter cette fonction. Une expérimentation et une bonne dose de profilage aller un long chemin. Une approche simple mais naïve est de supposer que la chaîne est vide ou trop petite pour contenir le résultat mis en forme. Je serait dans ce cas, commencer par déterminer la taille requise avec StringPrint, redimensionner la mémoire tampon pour correspondre et puis appelez StringPrint encore une fois avec le tampon alloué correctement. Quelque chose comme ceci :
size_t const size = StringPrint(nullptr, 0, format, args ...);
buffer.resize(size);
StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
Le + 1 est requis car les snprintf et swprintf deviner la taille du tampon déclarés inclut l'espace pour la marque de fin null. Cela fonctionne assez bien, mais il devrait être évident que je pars de performance sur la table. Une approche beaucoup plus rapide dans la plupart des cas est de supposer que la chaîne est suffisamment grande pour contenir le résultat mis en forme et redimensionner uniquement si nécessaire. Cela presque inverse le code précédent, mais est tout à fait sûr. J'ai commencer en essayant de la chaîne de format directement dans la mémoire tampon :
size_t const size = StringPrint(&buffer[0],
buffer.size() + 1,
format,
args ...);
Si la chaîne est vide dans un premier temps ou tout simplement pas assez grand, la taille résultante sera supérieure à la taille de la chaîne et je saurai pour redimensionner la chaîne avant d'appeler à nouveau les StringPrint :
if (size > buffer.size())
{
buffer.resize(size);
StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
}
Si la taille est inférieure à la taille de la chaîne, je saurai que le format a réussi, mais la mémoire tampon doit être coupé pour correspondre à :
else if (size < buffer.size())
{
buffer.resize(size);
}
Enfin, si les tailles correspondent il n'y a rien à voir, et la fonction de Format peut simplement retourner. Le modèle de fonction Format complet se trouve dans Figure 3. Si vous êtes familier avec la classe string, vous vous souvenez peut-être qu'il rend compte également de sa capacité et vous pourriez être tenté pour définir la taille de la corde pour correspondre à ses capacités avant d'appeler StringPrint la première fois, pensant que cela pourrait améliorer vos chances de mise en forme la chaîne correctement la première fois. La question est si un objet string peut être redimensionné plus vite que printf peut analyser la chaîne de format et calculer la taille requise. D'après mes tests informels, la réponse est : ça dépend. Vous voyez, redimensionnement d'une chaîne en fonction de sa capacité est plus qu'une simple modification de la taille signalée. Tous les caractères supplémentaires doivent être effacées et cela prend du temps. Si cela prend plus de temps qu'il faut printf pour analyser la chaîne de format dépend de combien de caractères doivent être dédouanés et comment complexe la mise en forme qui se trouve être. J'utilise un algorithme encore plus rapide pour le haut -volume de sortie, mais j'ai constaté que la fonction Format dans Figure 3 fournit de bonnes performances pour la plupart des scénarios.
Figure 3 chaînes de mise en forme
template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
T const * const format,
Args const & ... args)
{
size_t const size = StringPrint(&buffer[0],
buffer.size() + 1,
format,
args ...);
if (size > buffer.size())
{
buffer.resize(size);
StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
}
else if (size < buffer.size())
{
buffer.resize(size);
}
}
Avec cette fonction de Format dans la main, il devient très facile d'écrire des diverses fonctions d'assistance pour les opérations courantes de mise en forme de chaîne. Vous devrez peut-être convertir une chaîne de caractères en une chaîne ordinaire :
inline std::string ToString(wchar_t const * value)
{
std::string result;
Format(result, "%ls", value);
return result;
}
ASSERT("hello" == ToString(L"hello"));
Peut-être vous avez besoin pour mettre en forme des nombres à virgule flottante :
inline std::string ToString(double const value,
unsigned const precision = 6)
{
std::string result;
Format(result, "%.*f", precision, value);
return result;
}
ASSERT("123.46" == ToString(123.456, 2));
Pour la performance obsédée, ces fonctions de conversion spécialisés sont aussi assez faciles d'optimiser davantage parce que les tailles des mémoires tampon requise sont un peu prévisibles, mais je laisse ça à explorer sur votre propre.
Il s'agit d'une poignée de fonctions utiles de ma bibliothèque de sortie C++ moderne. J'espère qu'ils vous ont donné peu d'inspiration pour l'utilisation de C++ modernes pour mettre à jour certaines techniques programmation C et C++ de vieille école. Par ailleurs, ma bibliothèque de sortie définit les fonctions de l'argumentation, ainsi que les fonctions de bas niveau StringPrint dans un espace de noms imbriqué interne. Ce qui tend à maintenir la bibliothèque agréable et simple à découvrir, mais vous pouvez organiser votre application mais que vous le souhaitez.
Kenny Kerr est un programmeur informatique basé au Canada, mais aussi un auteur pour Pluralsight et MVP Microsoft. Il blogs à kennykerr.ca et vous pouvez le suivre sur Twitter à twitter.com/kennykerr.