Guru Of the Week n° 48 : changements de flux

| Herb Sutter | Traduction par l'équipe de la rubrique C++ |

Difficulté : 2 / 10
Quelle est la meilleure façon d'alterner entre différentes sources de flux et cibles, y compris les flux de console standards et les fichiers ?

Retrouver l'ensemble des Guru of the Week sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Guru Of the Week n° 48 : changements de flux

- Auteurs : Herb Sutter, Traduction par l'équipe de la rubrique C++

- Parution : 13 septembre 2012

- Licence : Copyright ® 2009 Herb Sutter. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Problème

- I-A. Question JG

- I-B. Question Guru

- II. Solution

- II-A. Quels sont les types de std::cin et de std::cout ?

- II-B. Éc rivez un programme ECHO qui renvoie simplement sa saisie et qui puisse être invoqué de deux façons :

- II-B-1. Méthode 0 : La solution dépouillée

- II-B-1-a. Des solutions plus souples

- II-B-2. Méthode 1 : Templates (polymorphisme au moment de la compilation)

- II-B-3. Méthode 2 : Fonctions virtuelles (polymorphisme au moment de faire tourner le programme)

- II-B-3-a. Bons principes d'ingénierie

- II-B-3-a-i. Préférer l'extensibilité

- II-B-3-a-ii. Préférer l'incorporation. Séparez les éléments traités

- III. Remerciements

I - Problème

I-A - Question JG

1. Quels sont les types de std::cin et de std::cout ?

I-B - Question Guru

2. écrivez un programme ECHO qui renvoie simplement sa saisie et qui puisse être invoqué de deux façons :

ECHO <infile >outfile

ECHO infile outfile

Dans la plupart des environnements populaires à lignes de commande, la première commande présume que le programme extrait sa saisie de std::cin et envoie son résultat sur std::cout. La seconde commande dit au programme de tirer sa saisie du fichier appelé "infile" et de produire son résultat dans le fichier appelé "outfile". Le programme doit être capable de supporter toutes les options d'entrée/sortie ci-dessus.

II - Solution

II-A - Quels sont les types de std::cin et de std::cout ?

Pour faire court, cin est :

std::basic_istream<char, std::char_traits<char> >

et cout est :

std::basic_ostream<char, std::char_traits<char> >

Notez : Si vous utilisez une plus ancienne implémentation du sous-système iostreams, vous verrez peut-être encore des classes intermédiaires telles que istream_with_assign. Ces classes n'apparaissent pas dans la norme finale, et votre implémentation devrait rapidement les éliminer, si ce n'est pas déjà fait.

II-B - Éc rivez un programme ECHO qui renvoie simplement sa saisie et qui puisse être invoqué de deux façons :

ECHO <infile >outfile

ECHO infile outfile

Dans la plupart des environnements populaires à lignes de commande, la première commande présume que le programme extrait sa saisie de std::cin et envoie son résultat sur std::cout. La seconde commande dit au programme de tirer sa saisie du fichier appelé "infile" et de produire son résultat dans le fichier appelé "outfile". Le programme doit être capable de supporter toutes les options d'entrée/sortie ci-dessus.

II-B-1 - Méthode 0 : La solution dépouillée

La solution dépouillée est une unique déclaration :

#include <fstream>
#include <iostream>
using namespace std;

int main( int argc, char* argv[] ) {
(argc>2
? ofstream(argv[2], ios::out | ios::binary)
: cout)
<<
(argc>1
? ifstream(argv[1], ios::in | ios::binary)
: cin)
.rdbuf();
}

Elle marche parce que basic_ios fournit une fonction pratique rdbuf() qui retourne le streambuf utilisé dans un objet de flux donné. Presque tout dans le sous-système de flux d'entrées-sorties dérive du template de classe basic_ios. En particulier, cela inclut cin, cout et les templates ifstream et ofstream.

II-B-1-a - Des solutions plus souples

La méthode 0 a deux inconvénients majeurs : D'abord le dépouillement est borderline, et un dépouillement extrême ne convient pas à un code de production. à partir des normes de codage GotW :

- programmez style:

- préférez une solution bien lisible :

- évitez d'écrire votre code de façon trop concise (brève mais difficile à comprendre et à entretenir) ; évitez la confusion (Sutter97b)

Ensuite, bien que la méthode 0 réponde à la question immédiate, ce n'est bon que lorsque vous voulez copier le verbatim d'entrée. C'est peut-être suffisant aujourd'hui, mais que se passera-t-il demain si vous avez besoin d'appliquer un autre traitement aux données en entrée, comme les convertir en majuscule, calculer un total ou enlever un caractère sur trois ? Il se peut que ce soit quelque chose de raisonnable à faire à l'avenir, alors vous avez intérêt à incorporer le process dès maintenant dans une fonction séparée qui puisse utiliser le bon type d'objet d'entrée ou sortie de façon polymorphe :

#include <fstream>
#include <iostream>
using namespace std;

int main( int argc, char* argv[] ) {
Process(
(argc>1
? ifstream(argv[1], ios::in  | ios::binary)
: cin ),
(argc>2
? ofstream(argv[2], ios::out | ios::binary)
: cout)
);
}

Mais comment implémenter Process() ? Sous C++, il y a deux façons utiles d'exprimer le polymorphisme :

II-B-2 - Méthode 1 : Templates (polymorphisme au moment de la compilation)

La première façon de faire est d'utiliser le polymorphisme au moment de la compilation en utilisant des templates, ce qui nécessite simplement que les objets passés aient une interface adéquate (comme une fonction membre appelée rdbuf) :

template<class In, class Out>
void Process( In& in, Out& out ) {
// ... fait quelque chose de plus sophistiqué,
//     ou juste "out << in.rdbuf();"...
}

II-B-3 - Méthode 2 : Fonctions virtuelles (polymorphisme au moment de faire tourner le programme)

La deuxième façon consiste à utiliser le polymorphisme au moment de faire tourner le programme, ce qui exploite le fait qu'il existe une classe de base commune avec une interface adéquate :

//  Méthode 2(a): Première tentative, qui marche
//
void Process( basic_istream<char>& in,
basic_ostream<char>& out ) {
// ... fait quelque chose de plus sophistiqué,
//     ou juste "out << in.rdbuf();"...
}

(les paramètres ne sont pas du type basic_ios<char>& parce que ça ne permettrait pas d'utiliser l'opérateur <<).

Bien sûr, ça dépend des flux d'entrée et sortie dérivant de basic_istream<char> et de basic_ostream<char>. Il se trouve que c'est assez bon pour servir d'exemple, mais tous les flux ne sont pas basés sur des caractères nus ni même sur char_traits<char>. Par exemple, de grands flux de caractères sont basés sur wchar_t, et GotW n°29 a montré l'utilité de traités définis par l'utilisateur avec différents comportements (ci_char_traits, pour l'insensibilité à la casse).

Même la méthode 2 devrait utiliser des templates et laisser le compilateur déduire les arguments de façon appropriée :

//  Méthode 2(b): Meilleure solution.
//
template<class C = char, class T = char_traits<C> >
void Process( basic_istream<C,T>& in,
basic_ostream<C,T>& out ) {
// ... fait quelque chose de plus sophistiqué,
//     ou juste "out << in.rdbuf();"...
}

II-B-3-a - Bons principes d'ingénierie

Toutes ces réponses sont "justes" jusqu'ici, mais dans cette situation, j'ai personnellement tendance à préférer la méthode 1, à cause de deux lignes de conduite précieuses :

II-B-3-a-i - Préférer l'extensibilité

évitez d'écrire un code qui ne résolve que le problème immédiat. écrire une solution extensible vaut presque toujours mieux (ne vous emballez pas).

Un jugement équilibré est un signe caractéristique du programmeur expérimenté. En particulier, les programmeurs expérimentés comprennent comment trouver le bon équilibre entre l'écriture de codes spécifiques qui ne résolvent que le problème immédiat (à courte vue, difficile à étendre) et l'écriture d'un cadre général grandiose pour résoudre ce qui devrait être un problème simple (surdimensionnement fanatique).

La méthode 1 n'est que légèrement plus complexe que la méthode 0, mais ce surplus de complexité vous donne une meilleure extensibilité. Elle est à la fois plus simple et plus souple que la méthode 2 ; elle s'adapte mieux aux nouvelles situations parce qu'elle évite d'être câblé pour travailler avec la seule hiérarchie des iostreams.

Donc, préférez l'extensibilité. Notez que CE N'EST PAS une autorisation pour se laisser aller et surdimensionner ce qui ne devrait être qu'un système simple. C'est néanmoins un encouragement à en faire plus que seulement résoudre le problème immédiat quand, en y songeant un peu, vous découvrez que le problème que vous êtes en train de résoudre n'est qu'un cas particulier d'un problème plus général. C'est particulièrement vrai dans la mesure où une conception extensible signifie souvent implicitement une conception favorable à l'incorporation :

II-B-3-a-ii - Préférer l'incorporation. Séparez les éléments traités

On peut soutenir que mieux que toute autre, la méthode 1 montre une bonne séparation des éléments traités : le code qui traite des possibles différences dans les sources en entrée/sortie est séparé du code qui traite comment effectivement faire le travail. C'est un deuxième attribut de la bonne ingénierie.

III - Remerciements

Cet article est une traduction en français par l'équipe de la rubrique C++ de l'article de Herb Sutter publié sur Guru of the Week. Vous pouvez retrouver cet article dans sa version originale sur le site de Guru of the Week : Switching Streams.