Cet article sera court et inspiré d'une des stratégies typiques de la bibliothèque Boost. Des thématiques passionnantes et stimulantes pour l'esprit, mais définitivement du code de grande personne. Notez aussi qu'une version plus efficace du lexical_cast décrit par la présente est proposée dans cet article mais il serait sage de lire et de comprendre la présente d'abord. Les sources complètes de la version plus efficace sont disponibles ici.
Une question fréquemment posée est comment peut-on convertir aisément un entier en texte et du texte en un entier? L'un de mes articles dans la rubrique Au secours Pat! répond à cette question sous plusieurs angles pour Java, les langages .NET et C++ (voir cette rubrique pour des détails).
Par souci de clarté, je reprendrai brièvement ici la solution que je propose pour C++ puis je couvrirai une version simple de ce que propose Boost pour ouvrir les portes d'une réflexion élargie. Les fichiers d'en-tête <string> et <sstream> doivent être inclus dans chaque cas.
De texte à autre chose | D'autre chose à texte |
---|---|
|
|
Dans la version à gauche, le contrat du type T est d'exposer un constructeur par défaut et un constructeur par copie, de même que de pouvoir être extrait d'un flux. Dans la version à droite, le contrat du type T est de pouvoir être projeté sur un flux de manière à pouvoir être représenté sous forme de texte.
Un programme utilisant ces outils pourrait être le suivant. La notation est (délibérément) inspirée des fonctions atoi() et itoa() du langage C, que bien des gens connaissent, et l'intention est (ouvertement) de faciliter la transition de programmeurs C (ou du moins inspirés par la tradition C) vers une approche plus générique, moins dangereuse et tout aussi efficace. Une approche préférable serait celle mettant de l'avant que l'intention manifeste est ici de réaliser une conversion explicite de types sécurisée selon des règles bien définies. Une stratégie pour y arriver serait d'exprimer ces deux fonctions de conversion sous la forme (apparente) d'opérateurs de conversion explicites de types respectant la forme des opérateurs ISO standards. |
|
Voici comment y arriver :
La réécriture de notre programme de test de manière à exploiter des opérateurs (apparents) de conversion explicite de types plutôt que des fonctions, de même que l'écriture des opérateurs (manifestes) eux-mêmes, donnerait en gros ce qui suit. Ce code souffre d'une déficience fonctionnelle du point de vue de la désérialisation du fait que certains objets complexes ne se convertissent pas bien en std::string du fait que leur forme sérialisée comprend des caractères d'espacement, mais cela donne quand même une idée de ce qu'il nous est possible de faire. Une version plus générale est possible mais demande plus de travail. |
|
Définir des fonctions sous la forme d'une conversion explicite de type... Étrange idée. Est-ce seulement une question de style ou y a-t-il anguille sous roche? Y a-t-il une réflexion plus profonde qui se cache derrière cette approche?
En fait, oui. Les conversions explicites de types ISO représentent un réel progrès pour ce qui est de la clarté du code, au sens où les intentions des programmeurs en sont clarifiées même dans les points a priori les plus nébuleux d'un programme, de même que pour ce qui est de la capacité du compilateur de générer du code de qualité (plus le compilateur comprendra l'intention et mieux il est en mesure de la réaliser).
Pour certains cas pathologiques, cela dit, les conversions explicites de types générales ne peuvent pas faire le travail réellement désiré. Pensons par exemple à un objet partageable permettant d'encapsuler un pointeur et de ne le supprimer que lorsque plus personne n'y pointe (avec un mécanisme pour compter les références sur cet objet comme on le voit par exemple avec le modèle COM) :
#include <iostream>
#include <string>
#include <memory>
int main() {
using namespace std;
auto ps0 = make_shared<string>("Yo");
auto ps1 = ps0;
cout << *ps0 << '\n' << *ps1 << endl;
*ps0 = "Coucou!";
cout << *ps0 << '\n' << *ps1 << endl;
cout << ps0->size() << endl;
}
Le passage par un flux et le recours à une stratégie homogène de traduction entraîne un coût à l'exécution. Ainsi, si on considère le programme de test ci-dessous :
#include <cstdio>
#include <string>
#include <iostream>
#include <chrono>
#include <sstream>
using namespace std;
using namespace std::chrono;
void parse(const string & in, unsigned int &out) {
sscanf(in.c_str(), "%u", &out);
}
template <class D, class S>
D lexical_cast(const S &src) {
using std::stringstream;
stringstream sstr;
sstr << src;
D dest;
sstr >> dest;
return dest;
}
int main() {
const int NTESTS = 1'000'000;
string s = "3";
{
unsigned long l = {};
auto avant = system_clock::now();
for (int i = 0; i < NTESTS; ++i) {
unsigned int ui;
parse(s, ui);
l += ui;
}
auto apres = system_clock::now();
cout << "on s'en fout: " << l << '\n';
cout << "Nb. ms pour " << NTESTS << " appels à parse(): "
<< duration_cast<milliseconds>(apres - avant) << endl;
}
{
unsigned long l = {};
auto avant = system_clock::now();
for (int i = 0; i < NTESTS; ++i)
l += lexical_cast<unsigned int>(s);
auto apres = system_clock::now();
cout << "on s'en fout: " << l << '\n';
cout << "Nb. tics pour " << NTESTS << " appels à lexical_cast<unsigned int>: "
<< duration_cast<milliseconds>(apres - avant) << endl;
}
}
...le test sur lexical_cast<unsigned int> prend approximativement dix fois le temps du test avec parse(const std::string&, unsigned int&), et ce lorsque le projet est compilé en mode de production (Release). La différence est beaucoup plus grande (mais en même temps moins significative) en mode Debug.
La différence de coût tient surtout de la création et de la destruction d'un flux lors de chaque invocation de lexical_cast. Si le choix est fait d'utiliser un flux global (exemple un peu malpropre mais simple) :
#include <cstdio>
#include <string>
#include <chrono>
#include <sstream>
#include <iostream>
using namespace std;
using namespace std::chrono;
void parse(const string &in, unsigned int &out) {
sscanf(in.c_str(), "%u", &out);
}
namespace {
stringstream sstr; // le flux
};
template <class D, class S>
D lexical_cast(const S &src) {
::str.clear();
::str.ignore();
::sstr << src;
D dest;
::sstr >> dest;
return dest;
}
int main() {
const int NTESTS = 1'000'000;
string s = "3";
{
unsigned long l = {};
auto avant = system_clock::now();
for (int i = 0; i < NTESTS; ++i) {
unsigned int ui;
parse(s, ui);
l += ui;
}
auto apres = system_clock::now();
cout << "on s'en fout: " << l << '\n';
cout << "Nb. ms pour " << NTESTS << " appels à parse(): "
<< duration_cast<milliseconds>(apres - avant) << endl;
}
{
unsigned long l = {};
auto avant = system_clock::now();
for (int i = 0; i < NTESTS; ++i)
l += lexical_cast<unsigned int>(s);
auto apres = system_clock::now();
cout << "on s'en fout: " << l << '\n';
cout << "Nb. tics pour " << NTESTS << " appels à lexical_cast<unsigned int>: "
<< duration_cast<milliseconds>(apres - avant) << endl;
}
}
... alors lexical_cast devient plus rapide (un appel à lexical_cast prend la moitié du temps de processeur que nécessite un appel à parse()) que des invocations à la pièce de parse(), mais le code devient plus sensible, en particulier si plusieurs threads doivent accéder à l'objet devenu globalement accessible. Utiliser un flux privé et des qualifications friend serait la meilleure option; je vous laisse cette stratégie en exercice.
Depuis C++ 11, on trouve des fonctions std::to_string() et std::to_wstring() pour convertir de manière standardisée une donnée d'un type quelconque à une std::string ou à une std::wstring. Ces fonctions sont définies pour tous les types primitifs numériques. Pour le chemin inverse, il existe une gamme de fonctions nommées std::stoi() (string to int), std::stof() (string to float), std::strtoull() (string to unsigned long long), etc.
Pour plus de détails, voir :
L'irritant principal de ces outils est que la conversion vers une string se prête à du code générique (un seul nom, peu importe le type du paramètre), mais ce n'est pas le cas de la conversion à partir d'une string – l'approche proposée ici pour lexical_cast est plus à propos pour du code générique.
Pour une critique du passage de atoi() à strtol(), voir ce texte de Russel Harmon en 2014 : http://rus.har.mn/blog/2014-05-19/strtol-error-checking/