Vous avez sûrement déjà voulu écrire un programme comme celui-ci :
#include <iostream>
int main() {
using namespace std;
cout << "Dévoilez les accents!" << endl;
}
...pour voir apparaître à la console, lors de l'exécution, quelque chose comme :
DÚvoilez les accents!
Remarquez que le caractère accentué 'é' apparaît à la console comme un 'Ú' (ou sous une autre forme inattendue) lorsqu'on cherche à l'afficher. Ce problème n'est pas unique au 'é', évidemment, et se répercute à bien des niveaux. Ce n'est pas non plus un problème dû à l'affichage du mauvais accent – je vous invite par exemple à examiner ce que produira à la console le même programme si vous remplacez le message "Dévoilez les accents!" par "À bientôt".
Il existe une solution simple à ce problème, mais c'est un de mes étudiants en informatique de 2016, Alexandre-Xavier Labonté-Lamoureux, qui me l'a appris (je sais beaucoup de choses, mais j'en ai encore à apprendre!), soit :
#include <iostream>
#include <locale>
using namespace std;
int main() {
locale::global(locale{""});
cout << "Dévoilez les accents!" << endl;
}
Ce qui suit relate ma démarche, au tout début des années 2000, pour résoudre ce problème sans connaître la solution toute simple qui vient d'être présentée.
La problématique ici en est une de catégories de caractères. Les caractères manipulés dans un programme C++ sont habituellement des char, auxquels est typiquement donnée une interprétation à partir de la table ASCII, alors que la console affiche des caractères respectant le standard Microsoft Windows nommé Original Equipement Manufacturer (OEM) Format.
Il n'y a pas vraiment de différences entre ces deux formats pour les caractères ayant une valeur se situant inclusivement entre 0 et 127, mais pour les caractères hors de cette plage les choses se compliquent du fait qu'une même valeur peut avoir un sens selon un encodage et un sens complètement différent selon l'autre encodage. C'est ce qui explique les différences à l'affichage ci-dessus.
Il faut donc, pour afficher correctement à la console Win32 une chaîne standard contenant des caractères accentués, convertir cette chaîne du standard ANSI ou ASCII au standard OEM.
Voici un programme de démonstration montrant comment procéder :
#include <windows.h>
#include <iostream>
#include <string>
#include <memory>
#include <iterator>
std::string FormaterPourConsole(const std::string &src) {
auto dest = std::make_unique<char[]>(src.size() + 1);
CharToOem(src.c_str(), &dest[0]);
return { &dest[0], &dest[src.size()] };
}
int main() {
using namespace std;
cout << FormaterPourConsole("Étonnant?") << endl;
}
Et voilà!
Si nous désirions rendre le tout relativement transparent, de manière à ce qu'écrire sur un flux de sortie tel que std::cout implique, dans le cas du texte, un formatage automatique des caractères, nous pourrions y arriver en créant notre propre version de cout, mais inséré dans un autre espace nommé que l'espace standard.
Je vous propose donc une implémentation faite il y a plusieurs années pour le cours 420KA0 (d'où l'espace nommé KA0) d'une classe nommée fluxsortie dont le rôle est d'encapsuler un flux de sortie standard (std::ostream) tel que std::cout et d'enrober les accès aux opérations d'écriture sur le flux (operator<<()) pour tous les types possibles de manière à relayer bêtement ces appels au flux encapsulé, sauf pour ceux impliquant une chaîne de caractères, à l'intérieur desquels nous insérerons le code requis pour parvenir au formatage attendu.
Vous remarquerez au passage deux caractéristiques importantes de l'implémentation proposée: elle est plutôt transparente (pour le code client, il suffit de remplacer using namespace std; par using namespace KA0; et le tour est joué) et elle est très rapide (toutes les méthodes sont inline et les accès indirects aux flux de sortie encapsulé se font à travers une référence):
La conversion de caractères ASCII ou ANSI à caractères OEM a été couverte plus haut. En voici une version un peu plus propre (et plus portable aux yeux du code client).
formatage_brut.h | formatage_brut.cpp |
---|---|
|
|
La classe KA0::fluxsortie tient à jour une référence à un flux standard (qui peut être std::cout mais peut évidemment aussi être tout flux standard, incluant un flux sur un fichier).
Un template explique que pour tout T (sauf cas particuliers, qui sont les spécialisations proposées par la suite) invoquer << sur un fluxsortie et un T implique de relayer la tâche d'affichage au flux standard encapsulé par notre petit flux.
Les spécialisations sur du texte prennent soin de relayer une version convenablement formatée du texte en question au flux.
//
// inclusions et using...
//
namespace KA0 {
class fluxsortie {
std::ostream &os;
public:
fluxsortie(std::ostream &os) : os{ os } {
}
template <class T>
fluxsortie& operator<<(T && val) {
os << val;
return *this;
}
template <>
fluxsortie& operator<(const char *s) {
os << (FormaterPourConsole(s));
return *this;
}
template <>
fluxsortie& operator<<(const std::string &s) {
os << (FormaterPourConsole(s));
return *this;
}
};
Le flux de sortie KA0::cout encapsule le flux de sortie standard std::cout. Notez ici l'utilisation d'une référence.
Pour simplifier le portrait, KA0::string sera un alias pour std::string.
fluxsortie cout{ std::cout };
using string = std::string;
}
Changer cette ligne pour using namespace std; suffit pour que le programme passe par std::cout directement plutôt que par KA0::cout.
using namespace KA0;
Pour le programme principal, pour le passage de std::cout à KA0::cout est absolument transparent.
int main() {
cout << "Étonnant?" << '\n' << "N'est-ce pas?" << 42 << '\n';
}
Cette solution est très opérationnelle, mais manque un peu de sophistication. En effet, compiler le programme suivant :
// ...
using namespace KA0;
int main() {
cout << "Étonnant?" // ok, tout va bien
<< '\n'; // ok; ça baigne!
}
... fonctionne sans problème, mais compiler le programme suivant :
// ...
using namespace KA0;
int main() {
using std::endl;
cout << "Étonnant?" // ok, tout va bien
<< endl; // ne compile pas, et l'erreur est compliquée!
}
... mènera à un ensemble de remarques cryptiques et désobligeantes de la part du compilateur. Clairement, nous ne sommes pas encore au stade du code de production.
Il existe un ensemble d'opérations applicables à un flux. Nous ne les mettrons pas toutes en place sur KA0::fluxsortie (cette classe, après tout, se destine à une simplification apparente des entrées/ sorties sur un flux standard pour un(e) étudiant(e) en début de formation) mais nous noterons que certaines des opérations prennent une forme subtile.
Par exemple :
|
|
Ces divers manipulateurs de flux ne sont pas tant des constantes que des objets (des foncteurs!) qui interagissent avec le flux où ils sont écrits. Passer un endl à un flux a pour effet de dire au endl d'agir sur le flux. En retour, endl écrit un caractère de changement de ligne sur le flux et demande au flux de réaliser immédiatement son écriture.
Là où l'on semble écrire une constante sur un flux, on passe en réalité une entité active et capable d'agir sur le flux.
Sachant cela, nous montrerons ici brièvement comment suppléer certaines des abstractions fondamentales des flux de sortie pour la classe KA0::fluxsortie; vous pourrez, au besoin, compléter le portrait à votre convenance.
À la base, la solution un peu plus complète sera fort similaire à l'ébauche de solution proposée plus haut. Quelques classes représenteront des manipulations de flux d'entrée/ sortie. Pour les fins de notre exemple, nous utiliserons les mêmes exemples que proposés précédemment :
CCes classes sont présentées dans l'exemple à droite. Dans chaque cas, une instance de la classe appropriée est exposée à même l'espace nommé de manière à correspondre à son équivalent dans l'espace nommé standard. Les changements de comportement d'un flux sont représentés par l'insertion d'une opération générique d'écriture sur un flux prenant en paramètre le concept approprié.
#ifndef KA0_STREAM_H
#define KA0_STREAM_H
#include "formatage_brut.h"
#include <iostream>
#include <iomanip>
#include <string>
namespace KA0 {
using string = std::string;
string FormaterPourConsole(const string &src);
class fluxsortie {
std::ostream &os;
public:
class FinLigne { };
class ChangementPrecision {
fluxsortie &flux;
public:
ChangementPrecision(fluxsortie &flux) : flux{ flux } {
}
ChangementPrecision& operator()(std::streamsize s) {
flux << std::setprecision(s);
return *this;
}
};
class ChangementLargeur {
fluxsortie &flux;
public:
ChangementLargeur(fluxsortie &flux) : flux{ flux } {
}
ChangementLargeur& operator()(std::streamsize s) {
flux << std::setw(s);
return *this;
}
};
class ChangementBase {
fluxsortie &flux;
public:
ChangementBase(fluxsortie &flux) : flux{ flux } {
}
ChangementBase& operator()(int base) {
flux << std::setbase(base);
return *this;
}
};
fluxsortie(std::ostream &os) : os{ os } {
}
template <class T>
fluxsortie& operator<<(T && val) {
os << val;
return *this;
}
template <>
fluxsortie& operator<<(const char *s) {
os << (FormaterPourConsole(s));
return *this;
}
template <>
fluxsortie& operator<<(const string &s) {
os << (FormaterPourConsole(s));
return *this;
}
template <>
fluxsortie& operator<<(const fluxsortie::FinLigne fl) {
os << std::endl;
return *this;
}
template <>
fluxsortie& operator<<(fluxsortie::ChangementPrecision) {
return *this;
}
template <>
fluxsortie& operator<<(fluxsortie::ChangementLargeur) {
return *this;
}
template <>
fluxsortie& operator<<(fluxsortie::ChangementBase) {
return *this;
}
};
using istream = std::istream;
extern fluxsortie::FinLigne endl;
extern fluxsortie cout;
extern istream &cin;
extern fluxsortie::ChangementPrecision setprecision;
extern fluxsortie::ChangementLargeur setw;
extern fluxsortie::ChangementBase setbase;
}
#endif
Le fichier source de notre bibliothèque d'entrées/ sorties formatées est relativement banal, ayant surtout pour rôle d'assurer la portabilité des concepts.
#include "ka0stream.h"
#include <windows.h>
#include <memory>
#include <string>
#include <iterator>
namespace KA0 {
string FormaterPourConsole(const string &src) {
auto dest = std::make_unique<char[]>(src.size() + 1);
CharToOem(src.c_str(), &dest[0]);
return { &dest[0], &dest[src.size()] };
}
fluxsortie::FinLigne endl;
fluxsortie cout(std::cout);
istream &cin(std::cin);
fluxsortie::ChangementPrecision setprecision(cout);
fluxsortie::ChangementLargeur setw(cout);
fluxsortie::ChangementBase setbase(cout);
}
Enfin, on utilisera chaque manipulateur de flux comme on le ferait normalement à partir des outils standard.
#include "ka0stream.h"
using namespace KA0;
int main() {
const double PI = 3.14159265358979;
string s = "Yo";
cout << "Étonnant?" << endl
<< "N'est-ce pas?" << 42 << endl
<< s << endl;
cout << 10 << endl;
cout << setbase(8) << 10 << endl;
cout << PI << endl;
cout << setw(15) << PI << endl;
cout << setprecision(12) << PI << endl;
cout << "Entrez un mot: ";
if (string mot; cin >> mot)
cout << "Vous avez entré " << mot << endl;
}
Technique relativement simple, donc, et d'une grande efficacité.
Notez que la technique utilisée pour remplacer les manipulateurs de flux comme setbase et autres est une idée, évidemment, mais qui a ses défauts (l'implémentation donnée ici est liée à un flux bien précis, ce qui manque cruellement de souplesse). Heureusement, on peut faire bien mieux. Voyez-vous comment?