Tout d'abord, il peut sembler que je demande des opinions subjectives, mais ce n'est pas ce que je recherche. J'aimerais entendre des arguments bien fondés sur ce sujet.
Dans l'espoir d'avoir un aperçu de la façon dont un framework de flux/sérialisation moderne devrait être conçu, je me suis récemment procuré une copie du livre IOStreams et locals C++ standard par Angelika Langer et Klaus Kreft . J'ai pensé que si IOStreams n'était pas bien conçu, il ne serait pas entré dans la bibliothèque standard C++ en premier lieu.
Après avoir lu différentes parties de ce livre, je commence à avoir des doutes si IOStreams peut se comparer à, par exemple. la STL d'un point de vue architectural global. Lire par exemple cette interview avec Alexander Stepanov ("l'inventeur" de la STL) pour en savoir plus sur certaines décisions de conception qui sont entrées dans la STL.
Ce qui me surprend en particulier :
On ne sait pas qui était responsable de la conception globale d'IOStreams (j'adorerais lire quelques informations générales à ce sujet - quelqu'un connaît-il de bonnes ressources?);
Une fois que vous plongez sous la surface immédiate des flux IOS, par ex. si vous souhaitez étendre IOStreams avec vos propres classes, vous obtenez une interface avec des noms de fonctions membres assez cryptiques et déroutants, par exemple getloc
/imbue
, uflow
/underflow
, snextc
/sbumpc
/sgetc
/sgetn
, pbase
/pptr
/epptr
(et il y a probablement des exemples encore pires). Il est donc beaucoup plus difficile de comprendre la conception globale et la façon dont les pièces individuelles coopèrent. Même le livre que j'ai mentionné ci-dessus n'aide pas ça beaucoup (à mon humble avis).
Ainsi ma question:
Si vous deviez juger par les normes d'ingénierie logicielle d'aujourd'hui (s'il y a en fait est un accord général à ce sujet), les flux IOS de C++ seraient-ils toujours considérés comme bien conçus? (Je ne voudrais pas améliorer mes compétences en conception de logiciels à partir de quelque chose qui est généralement considéré comme obsolète.)
Plusieurs idées mal conçues ont trouvé leur place dans la norme: auto_ptr
, vector<bool>
, valarray
et export
, pour n'en nommer que quelques-uns. Je ne prendrais donc pas nécessairement la présence d'IOStreams comme un signe de conception de qualité.
IOStreams a une histoire mouvementée. Ils sont en fait le remaniement d'une bibliothèque de flux antérieure, mais ont été créés à une époque où de nombreux idiomes C++ n'existaient pas, de sorte que les concepteurs n'ont pas bénéficié du recul. Un problème qui n'est devenu apparent qu'avec le temps était qu'il est presque impossible d'implémenter IOStreams aussi efficacement que stdio de C, en raison de l'utilisation abondante des fonctions virtuelles et de la transmission aux objets tampons internes même à la granularité la plus fine, et aussi grâce à une étrangeté insondable. dans la façon dont les paramètres régionaux sont définis et mis en œuvre. Ma mémoire est assez floue, je l'admets; Je me souviens qu'il a fait l'objet d'un débat intense il y a quelques années, sur comp.lang.c ++. Modéré.
En ce qui concerne qui les a conçus, la bibliothèque originale a été (sans surprise) créée par Bjarne Stroustrup, puis réimplémentée par Dave Presotto. Cela a ensuite été repensé et réimplémenté à nouveau par Jerry Schwarz pour Cfront 2.0, en utilisant l'idée de manipulateurs d'Andrew Koenig. La version standard de la bibliothèque est basée sur cette implémentation.
Source "La conception et l'évolution de C++", section 8.3.1.
Si vous deviez juger par rapport aux normes actuelles d'ingénierie logicielle (s'il existe réellement un accord général à ce sujet), les flux IOS de C++ seraient-ils toujours considérés comme bien conçus? (Je ne voudrais pas améliorer mes compétences en conception de logiciels à partir de quelque chose qui est généralement considéré comme obsolète.)
Je dirais [~ # ~] non [~ # ~] , pour plusieurs raisons:
Mauvaise gestion des erreurs
Les conditions d'erreur doivent être signalées avec des exceptions, pas avec operator void*
.
L'anti-modèle "objet zombie" est ce qui provoque des bugs comme ceux-ci .
Mauvaise séparation entre le formatage et les E/S
Cela rend les objets de flux inutiles, car ils doivent contenir des informations d'état supplémentaires pour le formatage, que vous en ayez besoin ou non.
Cela augmente également les chances d'écrire des bogues comme:
using namespace std; // I'm lazy.
cout << hex << setw(8) << setfill('0') << x << endl;
// Oops! Forgot to set the stream back to decimal mode.
Si au lieu de cela, vous avez écrit quelque chose comme:
cout << pad(to_hex(x), 8, '0') << endl;
Il n'y aurait pas de bits d'état liés au formatage et aucun problème.
Notez que dans les langages "modernes" comme Java, C # et Python, tous les objets ont un toString
/ToString
/__str__
fonction appelée par les routines d'E/S. AFAIK, seul C++ fait l'inverse en utilisant stringstream
comme méthode standard de conversion en chaîne.
Prise en charge médiocre d'i18n
La sortie basée sur Iostream divise les littéraux de chaîne en morceaux.
cout << "My name is " << name << " and I am " << occupation << " from " << hometown << endl;
Les chaînes de format mettent des phrases entières en littéraux de chaîne.
printf("My name is %s and I am %s from %s.\n", name, occupation, hometown);
Cette dernière approche est plus facile à adapter aux bibliothèques d'internationalisation comme GNU gettext, car l'utilisation de phrases entières fournit plus de contexte pour les traducteurs. Si votre routine de formatage de chaîne prend en charge le réordonnancement (comme le POSIX $
paramètres printf), il gère également mieux les différences dans l'ordre des mots entre les langues.
Je poste ceci comme une réponse séparée parce que c'est une pure opinion.
L'exécution des entrées et sorties (en particulier les entrées) est un problème très, très difficile, donc sans surprise la bibliothèque iostreams est pleine de bodges et de choses qui, avec un recul parfait, auraient pu être mieux faites. Mais il me semble que toutes les bibliothèques d'E/S, dans n'importe quelle langue, sont comme ça. Je n'ai jamais utilisé un langage de programmation où le système d'E/S était une chose de beauté qui m'a fait admirer son concepteur. La bibliothèque iostreams a des avantages, en particulier par rapport à la bibliothèque CI/O (extensibilité, sécurité de type, etc.), mais je ne pense pas que quiconque la présente comme un exemple de grande OO ou conception générique.
Mon opinion sur les iostreams C++ s'est considérablement améliorée au fil du temps, en particulier après que j'ai commencé à les étendre en implémentant mes propres classes de flux. J'ai commencé à apprécier l'extensibilité et la conception globale, malgré les noms de fonctions membres ridiculement pauvres comme xsputn
ou autre. Quoi qu'il en soit, je pense que les flux d'E/S sont une amélioration massive par rapport à C stdio.h, qui n'a pas de sécurité de type et est criblé de failles de sécurité majeures.
Je pense que le principal problème avec les flux IO est qu'ils confondent deux concepts liés mais quelque peu orthogonaux: le formatage textuel et la sérialisation. D'une part, les flux IO sont conçu pour produire une représentation textuelle formatée lisible par l'homme d'un objet, et d'autre part, pour sérialiser un objet dans un format portable. Parfois, ces deux objectifs sont un seul et même, mais d'autres fois cela entraîne des incongruités très ennuyeuses . Par exemple:
std::stringstream ss;
std::string output_string = "Hello world";
ss << output_string;
...
std::string input_string;
ss >> input_string;
std::cout << input_string;
Ici, ce que nous obtenons en entrée est pas ce que nous avons initialement sorti dans le flux. En effet, l'opérateur <<
Génère la chaîne entière, tandis que l'opérateur >>
Lit uniquement le flux jusqu'à ce qu'il rencontre un caractère d'espacement, car il n'y a pas d'informations longueur stocké dans le flux. Ainsi, même si nous générons un objet chaîne contenant "hello world", nous n'entrerons qu'un objet chaîne contenant "hello". Ainsi, bien que le flux ait rempli son rôle de facilité de formatage, il n'a pas réussi à sérialiser correctement puis à ne pas sérialiser l'objet.
Vous pourriez dire que IO n'étaient pas conçus pour être des installations de sérialisation, mais si tel est le cas, à quoi servent les flux input? D'ailleurs, dans la pratique, je/Les flux O sont souvent utilisés pour sérialiser des objets, car il n'y a pas d'autres fonctionnalités de sérialisation standard. Considérez boost::date_time
Ou boost::numeric::ublas::matrix
, Où si vous sortez un objet matrice avec l'opérateur <<
, vous obtiendrez la même matrice exacte lorsque vous la saisirez à l'aide de l'opérateur >>
. Mais pour ce faire, les concepteurs de Boost devaient stocker les informations de décompte de colonnes et de lignes sous forme de données textuelles dans la sortie, ce qui compromet l'affichage lisible par l'homme. Encore une fois, une combinaison maladroite des installations de formatage textuel et de la sérialisation.
Notez comment la plupart des autres langues séparent ces deux fonctions. En Java, par exemple, le formatage est effectué via la méthode toString()
, tandis que la sérialisation est effectuée via l'interface Serializable
.
À mon avis, la meilleure solution aurait été d'introduire des flux basés sur byte, à côté des flux basés sur character. Ces flux fonctionneraient sur des données binaires, sans souci de formatage/affichage lisible par l'homme. Ils pourraient être utilisés uniquement comme des installations de sérialisation/désérialisation, pour traduire des objets C++ en séquences d'octets portables.
j'ai toujours trouvé les IOStreams C++ mal conçus: leur implémentation rend très difficile la définition correcte d'un nouveau type de flux. ils ont également mélanger les fonctionnalités io et les fonctionnalités de formatage (pensez aux manipulateurs).
personnellement, la meilleure conception et mise en œuvre de flux que j'ai jamais trouvées réside dans le langage de programmation Ada. c'est un modèle de découplage, une joie de créer un nouveau type de flux, et les fonctions de sortie fonctionnent toujours quel que soit le flux utilisé. c'est grâce à un dénominateur le moins commun: vous sortez des octets dans un flux et c'est tout. les fonctions de flux prennent soin de mettre les octets dans le flux, ce n'est pas leur travail, par exemple formater un entier en hexadécimal (bien sûr, il existe un ensemble d'attributs de type, équivalent à un membre de classe, défini pour gérer la mise en forme)
j'aimerais que C++ soit aussi simple en ce qui concerne les flux ...
Je pense que la conception d'IOStreams est brillante en termes d'extensibilité et d'utilité.
Intégration de localisation et intégration de formatage. Voyez ce qui peut être fait:
std::cout << as::spellout << 100 << std::endl;
Peut imprimer: "cent" ou même:
std::cout << translate("Good morning") << std::endl;
Peut imprimer "Bonjour" ou "בוקר טוב" selon les paramètres régionaux imprégnés de std::cout
!
De telles choses peuvent être faites simplement parce que les flux ios sont très flexibles.
Pourrait-on faire mieux?
Bien sûr que oui! En fait, il y a beaucoup de choses qui pourraient être améliorées ...
Aujourd'hui, il est assez pénible de dériver correctement de stream_buffer
, il est assez simple d'ajouter des informations de mise en forme supplémentaires à diffuser, mais c'est possible.
Mais en regardant en arrière il y a de nombreuses années, la conception de la bibliothèque était encore assez bonne pour être sur le point d'apporter de nombreux goodies.
Parce que vous ne pouvez pas toujours voir la situation dans son ensemble, mais si vous laissez des points pour les extensions, cela vous donne de bien meilleures capacités même dans des points auxquels vous n'avez pas pensé.
(Cette réponse est juste basée sur mon opinion)
Je pense que les flux IOS sont beaucoup plus complexes que leurs équivalents de fonction. Lorsque j'écris en C++, j'utilise toujours les en-têtes cstdio pour les E/S "à l'ancienne", que je trouve beaucoup plus prévisibles. De plus, (bien que ce ne soit pas vraiment important; la différence de temps absolue est négligeable), les flux IOS se sont révélés à de nombreuses reprises plus lents que les E/S C.
Je rencontre toujours des surprises lors de l'utilisation de l'IOStream.
La bibliothèque semble orientée texte et non binaire. Cela peut être la première surprise: l'utilisation du drapeau binaire dans les flux de fichiers n'est pas suffisante pour obtenir un comportement binaire. L'utilisateur Charles Salvia ci-dessus l'a observé correctement: IOStreams mélange les aspects de formatage (où vous voulez une jolie sortie, par exemple des chiffres limités pour les flottants) avec les aspects de sérialisation (où vous ne voulez pas de perte d'informations). Il serait probablement bon de séparer ces aspects. Boost.Serialization fait cette moitié. Vous avez une fonction de sérialisation qui achemine vers les inserteurs et les extracteurs si vous le souhaitez. Là, vous avez déjà la tension entre les deux aspects.
De nombreuses fonctions ont également une sémantique déroutante (par exemple, get, getline, ignore and read. Certaines extraient le délimiteur, d'autres non; certaines définissent eof). Plus loin, certains mentionnent les noms de fonctions étranges lors de la mise en œuvre d'un flux (par exemple xsputn, uflow, underflow). Les choses empirent encore lorsque l'on utilise les variantes wchar_t. Le wifstream effectue une traduction en multioctets alors que wstringstream ne le fait pas. Les E/S binaires ne fonctionnent pas avec wchar_t: vous devez écraser le codecvt.
L'E/S tamponnée c (c'est-à-dire FILE) n'est pas aussi puissante que son homologue C++, mais est plus transparente et a un comportement beaucoup moins intuitif.
Toujours à chaque fois que je tombe sur l'IOStream, j'y suis attiré comme un papillon de nuit. Ce serait probablement une bonne chose si un gars vraiment intelligent avait un bon aperçu de l'architecture globale.
Les iostreams C++ ont beaucoup de défauts, comme indiqué dans les autres réponses, mais je voudrais noter quelque chose pour sa défense.
Le C++ est pratiquement unique parmi les langages très utilisés, ce qui rend les entrées et sorties variables simples pour les débutants. Dans d'autres langages, les entrées utilisateur impliquent généralement la coercition de type ou les formateurs de chaînes, tandis que C++ oblige le compilateur à faire tout le travail. La même chose est largement vraie pour la sortie, bien que C++ ne soit pas aussi unique à cet égard. Pourtant, vous pouvez très bien faire des E/S formatées en C++ sans avoir à comprendre les classes et les concepts orientés objet, ce qui est pédagogiquement utile, et sans avoir à comprendre la syntaxe des formats. Encore une fois, si vous enseignez aux débutants, c'est un gros plus.
Cette simplicité pour les débutants a un prix, ce qui peut en faire un casse-tête pour gérer les E/S dans des situations plus complexes, mais nous espérons que le programmeur en aura suffisamment appris pour pouvoir les gérer, ou du moins devenir assez vieux. boire.
Je ne peux m'empêcher de répondre à la première partie de la question (qui a fait ça?). Mais il a été répondu dans d'autres articles.
Quant à la deuxième partie de la question (bien conçue?), Ma réponse est un "non!" Retentissant. Voici un petit exemple qui me fait secouer la tête d'incrédulité depuis des années:
#include <stdint.h>
#include <iostream>
#include <vector>
// A small attempt in generic programming ;)
template <class _T>
void ShowVector( const char *title, const std::vector<_T> &v)
{
std::vector<_T>::const_iterator iter;
std::cout << title << " (" << v.size() << " elements): ";
for( iter = v.begin(); iter != v.end(); ++iter )
{
std::cout << (*iter) << " ";
}
std::cout << std::endl;
}
int main( int argc, const char * argv[] )
{
std::vector<uint8_t> byteVector;
std::vector<uint16_t> wordVector;
byteVector.Push_back( 42 );
wordVector.Push_back( 42 );
ShowVector( "Garbled bytes as characters output o.O", byteVector );
ShowVector( "With words, the numbers show as numbers.", wordVector );
return 0;
}
Le code ci-dessus produit un non-sens en raison de la conception d'iostream. Pour certaines raisons hors de ma portée, ils traitent les octets uint8_t comme des caractères, tandis que les types intégraux plus grands sont traités comme des nombres. Q.e.d. Mauvaise conception.
Il n'y a également aucun moyen de penser à résoudre ce problème. Le type pourrait aussi bien être un flottant ou un double à la place ... donc un cast à 'int' pour faire idiot iostream comprendre que les nombres et non les caractères sont le sujet n'aidera pas.
Après avoir reçu un vote négatif à ma réponse, peut-être quelques mots d'explication supplémentaires ... La conception IOStream est défectueuse car elle ne donne pas au programmeur un moyen d'indiquer COMMENT un élément est traité. L'implémentation IOStream prend des décisions arbitraires (comme traiter uint8_t comme un caractère, pas comme un nombre d'octets). Ceci IS une faille de la conception IOStream, car ils essaient de réaliser l'inatteignable.
C++ ne permet pas de classer un type - le langage n'a pas la possibilité. Il n'y a rien de tel que is_number_type () ou is_character_type () que IOStream pourrait utiliser pour faire un choix automatique raisonnable. Ignorer cela et essayer de s'en tirer en devinant IS un défaut de conception d'une bibliothèque.
Certes, printf () ne fonctionnerait pas non plus dans une implémentation générique "ShowVector ()". Mais ce n'est pas une excuse pour le comportement d'iostream. Mais il est très probable que dans le cas printf (), ShowVector () soit défini comme ceci:
template <class _T>
void ShowVector( const char *formatString, const char *title, const std::vector<_T> &v );