Cet article a fait l'objet d'une traduction automatique.
Windows et C++
Ajout de vérification pour Printf de Type compilation
J'ai exploré quelques techniques pour faire des printf plus commode à utiliser avec C++ moderne dans mon article de mars 2015 (msdn.microsoft.com/magazine/dn913181). J'ai montré comment transformer les arguments à l'aide d'un modèle de variadic afin de combler le fossé entre la classe de chaîne C++ officielle et la fonction printf archaïque. Pourquoi s'embêter ? Eh bien, printf est très rapide, et une solution au format de sortie qui peut tirer parti de que, tout en permettant aux développeurs d'écrire plus sûres, code de niveau supérieur est certainement souhaitable. Le résultat a été un modèle de fonction variadique Print qui pourrait prendre un programme simple comme ceci :
int main()
{
std::string const hello = "Hello";
std::wstring const world = L"World";
Print("%d %s %ls\n", 123, hello, world);
}
Et effectivement étendre la fonction printf interne comme suit :
printf("%d %s %ls\n", Argument(123), Argument(hello), Argument(world));
Le modèle de fonction Argument puis aussi compiler loin et laisser les fonctions d'accesseur nécessaires :
printf("%d %s %ls\n", 123, hello.c_str(), world.c_str());
Alors qu'il s'agit d'un pont commode entre C++ modernes et la fonction printf traditionnel, il ne fait rien pour résoudre les difficultés de la rédaction du code correct à l'aide de printf. printf est toujours printf et je compte sur le compilateur pas entièrement omniscient et bibliothèque à flairer des incohérences entre les spécificateurs de conversion et les arguments réels fournis par l'appelant.
C++ sûrement moderne peut faire mieux. De nombreux développeurs ont tenté de faire mieux. Le problème, c'est que les développeurs ont des exigences différentes ou des priorités. Certains sont heureux de prendre un coup de faible rendement et simplement compter sur < iostream > pour la vérification du type et de l'extensibilité. D'autres ont conçu des bibliothèques qui fournissent des fonctionnalités intéressantes qui nécessitent des structures supplémentaires et des allocations pour suivre l'état de mise en forme de. Personnellement, je ne suis pas satisfait des solutions qui introduisent des performances et une charge runtime pour ce qui devrait être une opération fondamentale et rapide. Si il est petit et rapide en C, il doit être petit, rapide et facile à utiliser correctement en C++. « Plus lent » ne doit pas entrer dans l'équation.
Alors que peut-on faire ? Eh bien, un peu abstrait est en ordre, aussi longtemps que les abstractions peuvent être compilées de suite pour laisser quelque chose de très proche une déclaration manuscrite printf. Il est important de réaliser que les spécificateurs de conversion tels que %d et %s sont vraiment juste des espaces réservés pour les arguments ou les valeurs qui suivent. Le problème, c'est que ces marques faire des hypothèses sur les types des arguments correspondants sans passer par aucun moyen de savoir si ces types sont corrects. Plutôt que d'essayer d'ajouter la vérification de type runtime qui va confirmer ces hypothèses, nous allons jeter ces informations de Pseudo- type et plutôt laisser le compilateur déduire les types d'arguments, comme ils sont naturellement résolus. Alors plutôt que de l'écriture :
printf("%s %d\n", "Hello", 2015);
Je devrais plutôt limiter la chaîne de format pour les caractères réels de sortie et tous les espaces réservés d'élargir. Je pourrais même utiliser le même caractère de remplacement comme espace réservé :
Write("% %\n", "Hello", 2015);
Il n'y a vraiment aucune information de type moins dans cette version que dans l'exemple précédent de printf. La seul printf raison nécessaire pour incorporer que les informations de type supplémentaires étaient parce que le langage de programmation C manquait variadic templates. Ce dernier est également beaucoup plus propre. Certes, je serais heureux si je n'avais pas à continuer à chercher jusqu'à plusieurs spécificateurs de format printf pour s'assurer que je l'ai juste comme il faut.
Je ne veux pas limiter la sortie à juste la console. Qu'en est-il de mise en forme dans une chaîne ? Qu'en est-il des autres cibles ? Un des défis de travailler avec printf est qu'alors qu'il prend en charge la sortie vers diverses cibles, il le fait par le biais de fonctions distinctes qui ne sont pas de surcharges et sont difficiles à utiliser de manière générique. Pour être juste, ne supporte pas le langage de programmation C programmation générique ou surcharges. Encore, nous allons pas répéter l'histoire. Je voudrais être en mesure d'imprimer à la console tout aussi facilement et de façon générique comme je peux imprimer une chaîne ou un fichier. Ce que j'ai à l'esprit est illustrée dans Figure 1.
Figure 1 sortie générique avec déduction de Type
Write(printf, "Hello %\n", 2015);
FILE * file = // Open file
Write(file, "Hello %\n", 2015);
std::string text;
Write(text, "Hello %\n", 2015);
Dans une perspective de conception ou de mise en œuvre, il devrait y avoir un modèle de fonction d'écriture qui agit comme un conducteur et de n'importe quel nombre de cibles ou des adaptateurs de cible qui sont liés de manière générique basé sur la cible identifiée par l'appelant. Un développeur doit alors être capable d'ajouter facilement des cibles supplémentaires au besoin. Alors, comment cela fonctionnerait-il ? Une option est un paramètre de modèle pour la cible. Quelque chose comme ceci :
template <typename Target, typename ... Args>
void Write(Target & target,
char const * const format, Args const & ... args)
{
target(format, args ...);
}
int main()
{
Write(printf, "Hello %d\n", 2015);
}
Cela fonctionne jusqu'à un point. Je peux écrire les autres cibles qui sont conformes à la signature attendue de printf et il devrait fonctionner assez bien. Je pourrais écrire un objet de fonction qui est conforme et ajoute la sortie d'une chaîne :
struct StringAdapter
{
std::string & Target;
template <typename ... Args>
void operator()(char const * const format, Args const & ... args)
{
// Append text
}
};
Je peux ensuite l'utiliser avec le même modèle de fonction d'écriture :
std::string target;
Write(StringAdapter{ target }, "Hello %d", 2015);
assert(target == "Hello 2015");
Dans un premier temps, cela peut sembler assez élégant et même souple. Je peux écrire toutes sortes de fonctions de l'adaptateur ou objets de fonction. Mais dans la pratique, il devient vite assez fastidieux. Il serait beaucoup plus souhaitable de simplement passer la chaîne comme cible directement et ont le modèle de fonction d'écriture de prendre soin de l'adapter selon son type. Donc, nous allons avoir le modèle de fonction d'écriture correspond à la cible demandé avec une paire de fonctions surchargées pour permettre l'ajout ou de mise en forme de sortie. Une petite compilation indirection va un long chemin. Se rendant compte qu'une grande partie de la production qui doive être rédigés est tout simplement être le texte sans tous les espaces réservés, j'ajouterai pas un, mais deux des surcharges. La première serait tout simplement ajouter du texte. Voici une fonction Append pour cordes :
void Append(std::string & target,
char const * const value, size_t const size)
{
target.append(value, size);
}
Je peux ensuite fournir une surcharge de Append pour printf :
template <typename P>
void Append(P target, char const * const value, size_t const size)
{
target("%.*s", size, value);
}
Je pourrais évité le modèle à l'aide d'un pointeur de fonction correspondant à la signature de printf, mais c'est un peu plus souple parce que les autres fonctions pourraient en théorie être liées à cette même mise en œuvre et le compilateur n'est pas entravé en quelque sorte par l'indirection de pointeur. Je peux également fournir une surcharge pour la sortie de fichier ou un flux :
void Append(FILE * target,
char const * const value, size_t const size)
{
fprintf(target, "%.*s", size, value);
}
Bien sûr, sortie formatée est toujours indispensable. Voici une fonction AppendFormat pour cordes :
template <typename ... Args>
void AppendFormat(std::string & target,
char const * const format, Args ... args)
{
int const back = target.size();
int const size = snprintf(nullptr, 0, format, args ...);
target.resize(back + size);
snprintf(&target[back], size + 1, format, args ...);
}
Elle détermine d'abord la quantité d'espace supplémentaire est nécessaire avant le redimensionnement de la cible et de mettre en forme le texte directement dans la chaîne. Il est tentant d'essayer d'éviter d'appeler snprintf deux fois en vérifiant s'il y a assez de place dans la mémoire tampon. J'ai tendance à toujours appeler snprintf deux fois est parce que les tests informels a indiqué que deux appels à elle est généralement moins cher que le redimensionnement à capacité. Même si une allocation n'est pas requise, ces caractères supplémentaires sont à zéro, qui tend à être plus cher. Cependant, c'est très subjectif, en fonction des profils de données et la fréquence à laquelle la chaîne cible est réutilisée. Et voici une pour printf :
template <typename P, typename ... Args>
void AppendFormat(P target, char const * const format, Args ... args)
{
target(format, args ...);
}
Une surcharge pour le fichier de sortie est tout aussi simple :
template <typename ... Args>
void AppendFormat(FILE * target,
char const * const format, Args ... args)
{
fprintf(target, format, args ...);
}
J'ai maintenant un des blocs de construction en place pour la fonction de pilote d'écriture. Les autres blocs de construction nécessaire est un moyen générique de manutention mise en forme d'argument. Alors que la technique que j'ai illustré dans mon article de mars 2015 était simple et élégant, il n'avait pas la capacité de faire face à toutes les valeurs qui ne correspondent directement aux types pris en charge par printf. Elle aussi ne pouvait pas traiter avec l'expansion de l'argument ou les valeurs d'arguments complexes tels que les types définis par l'utilisateur. Une fois de plus, un ensemble de fonctions surchargées peut résoudre le problème très élégamment. Supposons que la fonction de pilote d'écriture va passer chaque argument à une fonction WriteArgument. Voici un pour les chaînes :
template <typename Target>
void WriteArgument(Target & target, std::string const & value)
{
Append(target, value.c_str(), value.size());
}
Les différentes fonctions de WriteArgument toujours accepte deux arguments. Le premier représente la cible générique, tandis que le second est l'argument spécifique pour écrire. Ici, je me fonde sur l'existence d'une fonction d'ajout pour correspondre à la cible et de prendre soin de l'ajout de la valeur à la fin de la cible. La fonction WriteArgument n'a pas besoin de savoir ce qu'est réellement cette cible. Je pourrais éventuellement éviter les fonctions d'adaptateur cible, mais qui se traduirait par une augmentation quadratique WriteArgument surcharges. Voici une autre fonction de WriteArgument pour les arguments entiers :
template <typename Target>
void WriteArgument(Target & target, int const value)
{
AppendFormat(target, "%d", value);
}
Dans ce cas, la fonction de WriteArgument attend une fonction AppendFormat pour correspondre à la cible. Comme pour les surcharges Append et AppendFormat, fonctions supplémentaire WriteArgument d'écriture est simple. La beauté de cette approche est que les adaptateurs d'argument n'avez pas besoin de retourner une valeur vers le haut de la pile à la fonction printf, comme ils le faisaient dans la version de mars 2015. Au lieu de cela, les surcharges WriteArgument portée effectivement la sortie telle que la cible est écrit immédiatement. Cela signifie que les types complexes peuvent être utilisés comme arguments et stockage temporaire peut être invoqué même pour mettre en forme leur représentation textuelle. Voici une surcharge de WriteArgument pour les GUID :
template <typename Target>
void WriteArgument(Target & target, GUID const & value)
{
wchar_t buffer[39];
StringFromGUID2(value, buffer, _countof(buffer));
AppendFormat(target, "%.*ls", 36, buffer + 1);
}
Je pourrais même remplacer la fonction Windows StringFromGUID2 et formatez-la directement, peut-être pour améliorer les performances ou ajouter la portabilité, mais cela montre clairement la puissance et la flexibilité de cette approche. Types définis par l'utilisateur peuvent facilement être pris en charge avec l'ajout d'une surcharge de WriteArgument. J'ai appelé les surcharges ici, mais strictement parlant, ils ne doivent pas être. La bibliothèque de sortie peut certainement fournir un ensemble de surcharges pour les cibles et les arguments communs, mais la fonction de pilote d'écriture ne devraient pas assumer les fonctions de l'adaptateur sont surcharges et, au lieu de cela, doivent les traiter comme les non membres commencent et fonctions définies par la bibliothèque C++ Standard. Commencer le non-membre et fonctions sont extensible et adaptable à tous les types de conteneurs standards et non-standard, précisément parce qu'ils n'ont pas besoin de résider dans l'espace de noms std, mais devraient plutôt être locales à l'espace de noms du type étant mis en correspondance. De la même manière, ces fonctions adaptateur cible et l'argument doivent pouvoir résider dans d'autres espaces de noms à l'appui des objectifs du développeur et les arguments définis par l'utilisateur. Donc ce que la fonction de pilote écriture ressemble ? Pour commencer, il y a une seule fonction d'écriture :
template <typename Target, unsigned Count, typename ... Args>
void Write(Target & target,
char const (&format)[Count], Args const & ... args)
{
assert(Internal::CountPlaceholders(format) == sizeof ... (args));
Internal::Write(target, format, Count - 1, args ...);
}
La première chose qu'il doit faire est de déterminer si le nombre d'espaces réservés dans la chaîne de format est égal au nombre d'arguments dans le pack de paramètre variadique. Ici, j'utilise un assert de runtime, mais cela devrait vraiment être un static_assert qui vérifie la chaîne de format au moment de la compilation. Malheureusement, Visual C++ n'est pas là encore. Pourtant, je peux écrire le code pour que lorsque le compilateur rattrape, le code peut être facilement mis à jour pour vérifier la chaîne de format au moment de la compilation. Par conséquent, la fonction interne de CountPlaceholders devrait être un __classid :
constexpr unsigned CountPlaceholders(char const * const format)
{
return (*format == '%') +
(*format == '\0' ? 0 : CountPlaceholders(format + 1));
}
Lorsque Visual C++ permet d'obtenir une conformité complète avec C ++ 14, au moins en ce qui concerne les __classid, vous devriez être en mesure de remplacer simplement l'assertion à l'intérieur de la fonction Write avec static_assert. Ensuite, c'est à la fonction interne de Write surchargée de dérouler la sortie de l'argument spécifique au moment de la compilation. Ici, je peux compter sur le compilateur générera et appelez les surcharges nécessaires de la fonction d'écriture interne pour satisfaire le pack de paramètre variadique élargi :
template <typename Target, typename First, typename ... Rest>
void Write(Target & target, char const * const value,
size_t const size, First const & first, Rest const & ... rest)
{
// Magic goes here
}
Je vais me concentrer sur cette magie dans un instant. En fin de compte, le compilateur va manquer d'arguments et une surcharge de non-variadic est nécessaires pour terminer l'opération :
template <typename Target>
void Write(Target & target, char const * const value, size_t const size)
{
Append(target, value, size);
}
Les deux fonctions d'écriture internes acceptent une valeur, ainsi que de la taille de la valeur. Le modèle de fonction variadique écriture doit assumer davantage il y a au moins un espace réservé dans la valeur. La fonction d'écriture non-variadic ne besoin faire aucune telle hypothèse et pouvez utiliser simplement la fonction Append générique d'écrire n'importe quel partie de fin de la chaîne de format. Avant le variadic fonction Write peut écrire ses arguments, elle doit tout d'abord écrire tous les caractères principaux et, bien sûr, trouver le premier espace réservé ou caractère de remplacement :
size_t placeholder = 0;
while (value[placeholder] != '%')
{
++placeholder;
}
Alors seulement peut il écrire les caractères principaux :
assert(value[placeholder] == '%');
Append(target, value, placeholder);
Le premier argument peut alors s'écrire et le processus se répète jusqu'à ce qu'aucun nouvel argument et espaces réservés sont laissés :
WriteArgument(target, first);
Write(target, value + placeholder + 1, size - placeholder - 1, rest ...);
Je peux désormais en charge la sortie générique dans Figure 1. Je peux même convertir un GUID en chaîne tout simplement :
std::string text;
Write(text, "{%}", __uuidof(IUnknown));
assert(text == "{00000000-0000-0000-C000-000000000046}");
Qu'en est-il quelque chose d'un peu plus intéressant ? Que diriez-vous de visualiser un vecteur :
std::vector<int> const numbers{ 1, 2, 3, 4, 5, 6 };
std::string text;
Write(text, "{ % }", numbers);
assert(text == "{ 1, 2, 3, 4, 5, 6 }");
Pour cela, j'ai simplement besoin d'écrire un modèle de fonction de WriteArgument qui accepte un vecteur comme argument, comme le montre Figure 2.
La figure 2, visualisant un vecteur
template <typename Target, typename Value>
void WriteArgument(Target & target, std::vector<Value> const & values)
{
for (size_t i = 0; i != values.size(); ++i)
{
if (i != 0)
{
WriteArgument(target, ", ");
}
WriteArgument(target, values[i]);
}
}
Notez comment je t'ai pas obligé le type des éléments dans le vecteur. Cela signifie que je peux maintenant utiliser la même implémentation de visualiser un vecteur de chaînes :
std::vector<std::string> const names{ "Jim", "Jane", "June" };
std::string text;
Write(text, "{ % }", names);
assert(text == "{ Jim, Jane, June }");
Bien sûr, qui soulève la question suivante : Que se passe-t-il si je veux développer encore plus l'espace réservé ? Bien sûr, je peux écrire un WriteArgument pour un conteneur, mais il n'offre aucune marge de manœuvre dans le peaufinage de la sortie. Imaginons que j'ai besoin de définir une palette de jeu de couleurs de l'app et j'ai des couleurs primaires et couleurs secondaires :
std::vector<std::string> const primary = { "Red", "Green", "Blue" };
std::vector<std::string> const secondary = { "Cyan", "Yellow" };
La fonction Write ceci formatera un plaisir pour moi :
Write(printf,
"<Colors>%%</Colors>",
primary,
secondary);
La sortie, cependant, n'est pas tout à fait ce que je veux :
<Colors>Red, Green, BlueCyan, Yellow</Colors>
C'est évidemment faux. Au lieu de cela, je tiens à marquer les couleurs tel que je connais qui sont primaires et qui sont secondaires. Peut-être quelque chose comme ceci :
<Colors>
<Primary>Red</Primary>
<Primary>Green</Primary>
<Primary>Blue</Primary>
<Secondary>Cyan</Secondary>
<Secondary>Yellow</Secondary>
</Colors>
Nous allons ajouter une fonction WriteArgument plus que peut offrir ce niveau d'extensibilité :
template <typename Target, typename Arg>
void WriteArgument(Target & target, Arg const & value)
{
value(target);
}
Avis, que l'opération semble être renversé sur sa tête. Plutôt que de passer la valeur de la cible, la cible est passée à la valeur. De cette façon, je peux fournir une fonction liée comme un argument au lieu de juste valeur. Je peux joindre un comportement défini par l'utilisateur et non pas simplement une valeur définie par l'utilisateur. Voici une fonction WriteColors qui fait ce que je veux :
void WriteColors(int (*target)(char const *, ...),
std::vector<std::string> const & colors, std::string const & tag)
{
for (std::string const & color : colors)
{
Write(target, "<%>%</%>", tag, color, tag);
}
}
Remarquez ce n'est pas un modèle de fonction et je l'ai eu à essentiellement de coder pour une seule cible. C'est une personnalisation spécifique à la cible, mais il montre ce qui est possible même lorsque vous avez besoin de sortir de la déduction du type générique directement fournie par la fonction de pilote d'écriture. Mais comment il peut être incorporé dans une plus grande oper d'écritureation ? Eh bien, vous pourriez être tenté pour un moment d'écrire ceci :
Write(printf,
"<Colors>\n%%</Colors>\n",
WriteColors(printf, primary, "Primary"),
WriteColors(printf, secondary, "Secondary"));
En mettant de côté un instant le fait que cela ne sera pas compiler, il ne serait pas te donnent vraiment le bon déroulement des événements, en tout cas. Si cela devait travailler, les couleurs seraient imprimées avant l'ouverture < couleurs > Tag. De toute évidence, ils doivent être appelées comme s'ils étaient des arguments dans l'ordre dans lequel ils apparaissent. Et c'est ce que permet le nouveau modèle de fonction WriteArgument. J'ai juste besoin de lier les invocations WriteColors tels qu'ils peuvent être appelés à un stade ultérieur. Pour qui faire encore plus simple pour quelqu'un à l'aide de la fonction de pilote d'écriture, je peux offrir vers le haut un wrapper pratique bind :
template <typename F, typename ... Args>
auto Bind(F call, Args && ... args)
{
return std::bind(call, std::placeholders::_1,
std::forward<Args>(args) ...);
}
Ce modèle de fonction Bind s'assure simplement qu'un espace réservé est réservé pour la cible éventuelle à laquelle elle sera écrite. Je peux puis à juste titre formater ma palette de couleur comme suit :
Write(printf,
"<Colors>%%</Colors>\n",
Bind(WriteColors, std::ref(primary), "Primary"),
Bind(WriteColors, std::ref(secondary), "Secondary"));
Et j'obtiens le résultat attendu étiqueté. Les fonctions d'assistance ref ne sont pas strictement nécessaires, mais éviter de faire une copie des conteneurs pour les wrappers d'appel.
Pas convaincu ? Les possibilités sont infinies. Vous pouvez gérer les arguments de chaîne de caractère efficacement pour les caractères larges et normales :
template <typename Target, unsigned Count>
void WriteArgument(Target & target, char const (&value)[Count])
{
Append(target, value, Count - 1);
}
template <typename Target, unsigned Count>
void WriteArgument(Target & target, wchar_t const (&value)[Count])
{
AppendFormat(target, "%.*ls", Count - 1, value);
}
De cette façon que je peux écrire facilement et en toute sécurité en utilisant des caractères différents de sortie définit :
Write(printf, "% %", "Hello", L"World");
Que se passe-t-il si vous ne spécifiquement ou initialement besoin d'écrire la sortie, mais au lieu de cela suffit de calculer combien d'espace serait nécessaire ? Pas de problème, je peux créer simplement une nouvelle cible qui le résume :
void Append(size_t & target, char const *, size_t const size)
{
target += size;
}
template <typename ... Args>
void AppendFormat(size_t & target,
char const * const format, Args ... args)
{
target += snprintf(nullptr, 0, format, args ...);
}
Je peux maintenant calculer la taille choisie tout simplement :
size_t count = 0;
Write(count, "Hello %", 2015);
assert(count == strlen("Hello 2015"));
Je pense que c'est sûr de dire que cette solution résout enfin la fragilité de type inhérente à l'utilisation de printf directement tout en préservant la plupart des avantages de la performance. C++ moderne est plus en mesure de répondre aux besoins des développeurs à la recherche d'un environnement productif avec vérification tout en conservant la performance dont sont traditionnellement connue C et C++ de type fiable.
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.
Merci à l'expert technique suivante de Microsoft pour l'examen de cet article : James McNellis