OVH Cloud OVH Cloud

ofstream : fichiers binaires

18 réponses
Avatar
Jeremie Fouche
Bonjour a tous

Dans mon projet, je souhaite sauvegarder mes données sous plusieurs formats.
Pour cela, j'ai décider de dériver plusieurs classes de ofstream (autant que
de format), et de surcharger l'operator<< pour chacune. Pour les format de
type texte, il n'y a pas de problemes, mais pour les formats binaires, la
seul solution que j'ai trouvé me semble <tres> lourde, voyez plutot :

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

class A
{
public:
A():m_value(77) {}
virtual ~A() {}

public:
int m_value;
};

class oMadStream : public ofstream
{
public:
oMadStream(const char* sTitre)
{
open(sTitre, ios_base::out|ios_base::trunc|ios_base::binary);
}
};

oMadStream& operator<<(oMadStream& osMad, const A& rcA)
{
osMad << "mad";
osMad << static_cast<char> ((rcA.m_value )&0xFF);
osMad << static_cast<char> ((rcA.m_value>> 8)&0xFF);
osMad << static_cast<char> ((rcA.m_value>>16)&0xFF);
osMad << static_cast<char> ((rcA.m_value>>24)&0xFF);
return osMad;
}

ostream& operator<<(ostream& os, const A& rcA)
{
os << "m_value=" << rcA.m_value << endl;
return os;
}

int main(void)
{
A a;
cout << a;

oMadStream mad("test.mad");
if (mad.is_open())
{
mad << a;
mad.close();
}

return 0;
}

Rassurez moi, y a t il l'équivalent de la fonction C fwrite qui permettrait
de sauvegarder mes données au format binaires ?
Merci

--
Jérémie

8 réponses

1 2
Avatar
kanze
"Jeremie Fouche" wrote in message
news:<ccgvo4$k3r$...
James Kanze a écrit dans le message :

"Jeremie Fouche" writes:

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

|> class CMad
|> {
|> public:
|> CMad():m_value(15) {}
|> virtual ~CMad() {}

|> public:
|> int m_value;
|> };

|> class madStream : public ios
|> {
|> public:
|> explicit madStream(filebuf *flux)
^^^^^^^

Ici, j'utiliserais streambuf.


Pourquoi pas, mais en fait, le flux (dans le cas présent) est
forcément orienté fichier. Dans mon projet, j'ai 3 classes de flux
différents, 2 orientés fichiers, et 1 quelquonque. Pour info, je
travail sur des fichier musicaux : le format MID, le format MAD (mon
format particulier), et un format txt.


J'utiliserais quand même streambuf. Par principe.

En fait, la question est : quel est le contrat que tu veux établir pour
ta classe. S'il y a une forte raison interne à la classe pour insister
que l'utilisateur ne se sert jamais que des filebuf ; alors, il faut que
filebuf soit partie du contrat, et la meilleur façon pour le faire,
c'est d'insister qu'on a un pointeur à un filebuf. Mais j'imagine plutôt
que madStream peut s'en tirer très bien avec n'importe quel streambuf.
Dans ce cas-là, insister que ce soit un filebuf introduit une contrainte
artificielle. Contraite qui n'est peut-être pas genant actuellement,
mais qui sait en ce qui concerne l'avenir.

|> {
|> init(flux);
|> }
|> };

|> madStream& operator<<(madStream& oms, const CMad& rcMad)
|> {
|> oms.rdbuf()->sputc( (char) rcMad.m_value );

La conversion explicite n'est pas nécessaire, mais... Est-ce que ça
fait réelement ce que tu veux ?


oui pour le moment, car m_value est inferieur à 127 dans mon projet.


Là n'est pas la question. Tu écris dans un format externe. Si le format
externe veut un seul octet, il faut n'écrire qu'un seul octet. Si le
format externe en veut quate, il faut en écrire quatre. Même si les
valeurs passent toutes dans un octet.

Je ne peux pas insister assez là dessus. Le format externe est un
contrat. Un contrat inviolable -- si tu n'écris qu'un octet, et celui
qui lit en lit quatre, ça ne va pas marcher.

Mais la réponse que j'attend de ta part est tout simplement de
déclarer m_value en char je pense ! cf: une autre remarque plus bas.


Pas forcement. Je n'étais simplement pas sûr que tu comprenais les
implications. Si le format externe veut un octet, il ne faut en écrire
qu'un, c'est sûr.

Dans mon propre code, je n'écrirais jamais quelque chose de ce genre
sans une assertion, e.g. :
assert( rcMad.m_value < CHAR_MAX && rcMad.m_value >= 0 ) ;
Ou peut-être avec d'autres bornes -- tout dépend de ce qu'exige le
format.

Plus généralement, les operator<< de base doivent probablement se
modéler sur les ostream -- jusqu'au point de définir ta propre
classe de sentinelle. En gros : si on a déjà vu une erreur
quelconque, on n'essaie plus, et si on voit une erreur, il faut en
positionner le bit correspondant dans ios. Donc, en gros, pour
sortir un entier, j'écrirai quelque chose du genre (en supposant des
int de 32 bits) :

madStream&
operator<<( madStream& dest, int value )
{
madStream::sentry sentry( dest ) ;
if ( sentry ) {
streambuf* sb = dest.rdbuf() ;
unsigned tmp = value ;
int shift = 32 ;
while ( dest && shift > 0 ) {
if ( sb->sputc( (tmp >> shift) & 0xff ) == EOF ) {
dest.clear( std::ios::badbit ) ;
}
shift -= 8 ;
}
}
return dest ;
}

Ça, c'est pour écrire des int comme 4 octets, poids fort d'abord,
format complément à deux. C-à-d format Internet.


Ah oui, ok, je note. C'est <un peu> plus complet que ce que j'ai fait ;)


Il en écrit plus. Mais aussi, il gère les cas d'erreurs. C'est très
important aussi. Si le disque est plein, tu ne veux pas que ton
programme signale que tout c'est bien passé. L'idiome consacré, c'est
que badbit dans le flux de sortie signale de telles erreurs. On le
détecte en ce que sputc renvoie EOF.

Dans un premier temps au moins, madStream::sentry serait assez
simple ; quelque chose du genre :

class madStream::sentry
{
public:
sentry( madStream& dest )
: myDest( dest )
{
}
operator bool() const
{
return ! myDest.fail() ;
}

private:
madStream& myDest ;
}


Je n'avais pas trop compris le rome de sentry. Je vais faire des
essais pour comprendre un peu.


Dans ce cas précis, il ne fait pas grand chose. L'intérêt, c'est qu'il
est là, pour pouvoir faire quelque chose s'il faut.

En tout cas, s'il y a déjà eu une erreur, ce n'est pas la peine de
continuer. Même, on ne doit pas essayer de continuer.

Comme dans les iostream, je définirais d'abord un istream et un
ostream ; si j'ai besoin des flux bidirectionnels, j'ajouterais un
iostream dérivé des deux (mais dans l'ensemble, je préfèrerais
éviter la bidirectionnalité). Et si on prévoit la bidirectionnalité,
il faut dériver virtuellement de std::ios.


Bien, je suis d'accord.

|> {
|> rMad.m_value = ims.rdbuf()->sgetc();
|> return ims;
|> }

Ici aussi, tu ne lis qu'un seul caractère. Pire, tu ne signales
aucune erreur. Comme ci-dessus :

iMadStream&
operator>>( IMadStream& source, int& value )
{
IMadStream::sentry sentry( source ) ;
if ( sentry ) {
streambuf* sb = source.rdbuf() ;
unsigned result = 0 ;
int shift = 32 ;
int tmp = sb->sgetc() ;
while ( tmp != EOF && shift > 0 ) {
shift -= 8 ;
result = result | (tmp << shift) ;
tmp = sb->sgetc() ;
}
if ( shift != 0 ) {
if ( shift == 32 ) {
source.clear( std::ios::eofbit | std::ios::failbit ) ;
} else {
source.clear( std::ios::eofbit | std::ios::badbit ) ;
}
} else {
value = tmp ;
}
}
return source ;


heu, je copie, je colle, je vais faire quelque tests, et je pense que
je comprendrais un peu plus tard dans la journée ;)


Ici aussi, je lis quatre octets, dans l'ordre que veut l'Internet. Ce
qui complique les choses un peu, parce qu'une fin de fichier quand
j'essaie de lire le premier octet ne doit pas forcement avoir les mêmes
conséquences que quand j'essaie d'en lire un des suivants. Une fin de
fichier sur le premier octet n'est pas forcément une erreur -- ça peut
être une fin de fichier valide (selon le format). Tandis qu'une fin de
fichier au milieu d'un élément peut signifier seulement que le fichier
n'est pas conforme au format -- une vraie erreur.

Ici, j'avoue ne pas suivre les conventions des istream standard,
mais je ne sais pas comment faire autrement pour permettre à
l'utilisateur de distinguer entre une vraie fin de fichier, et une
erreur de formattage. (Mais beaucoup dépend du format binaire qu'on
implémente, et comment on sait s'il y a un entier qui suit ou non.)

Aussi, l'affectation de value à la fin n'est pas garantie par la
norme. Mais ça fonctionne avec toutes les implémentations modernes,
et je ne sais pas de tête comment l'écrire d'une façon portable.

|> ostream& operator<<(ostream& os, const CMad& rcMad)
|> {
|> os << "value=" << rcMad.m_value << endl;
|> return os;
|> }

|> int main(void)
|> {
|> CMad mad;
|> filebuf fb;
|> fb.open("mad", ios::out|ios::binary|ios::trunc);
|> if ( fb.is_open() )
|> {
|> madStream oms(&fb);
|> oms << mad;
|> fb.close();
|> }

|> mad.m_value = 10;

Il faudrait tester avec d'autres valeurs. Qu'est-ce que ça donne
avec 1000, par exemple ?


Toujours le meme probleme que précédement, j'utilise en réalité des
accesseurs qui me premettent de controller la valeur passée a m_value.

|> cout << mad;
|> filebuf fb2;
|> fb2.open("mad", ios::in|ios::binary);
|> if (fb2.is_open())
|> {
|> madStream ims(&fb2);
|> ims >> mad;
|> cout << mad;
|> fb2.close();
|> }
|> return 0;
|> }

|> La chose qui me gene est que j'aurai souhaité cacher le fait que
|> le filebuf doive etre ouvert en mode binaire. Peut etre mettre
|> l'initialisation du filebuf dans le constructeur de madStream
|> ie:

|> madStream(const char* sNom, madStream::openmode mode)
|> {
|> filebuf fb;
|> fb.open(mode|ios_base::mode_binary);
|> init(&fb);

Alors, ios va stocker un pointeur au fb qui va disparaître du moment
qu'on qui le constructeur. Pas une bonne idée de tout.


Non, effectivement, mais dans mes tests, le filebuf etait variable
membre de ma classe (on ne peut pas dire que c'était clair). Peut etre
n'est ce pas une bonne idée.


Ça dépend. J'ai bien un ou deux cas où le filebuf appartenait
logiquement à une classe, et alors, c'est là où je l'ai mis. Mais dans
l'ensemble, je trouve la solution des iostream plus propre dans la
plupart des cas.

La solution « consacrée », encore, c'est de se modéler sur les
iostream. Tu crées une classe dérivée, fMadStream (ou plutôt
ifMadStream et ofMadStream) qui s'occupe de la gestion du filebuf.
Quelque chose du genre :

class ifMadStream : private virtual std::filebuf, public iMadStream
...


Oula ! Je vais faire quelques essais, car je ne connais pas du tout
l'héritage virtuel.


Ici, il ne sert réelement que pour s'assurer que quoiqu'il arrive par la
suite, le filebuf serait initialisé avant d'entrer dans le constructeur
de iMadStream. Bien que... Si tu veux créer un madStream bidirectionnel,
en héritant à la fois de iMadStream et de oMadStream, l'héritage virtuel
va s'assurer qu'il n'y a qu'un seul filebuf, et non deux.

--
James Kanze GABI Software http://www.gabi-soft.fr
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Jeremie Fouche
James Kanze a écrit dans le message :


Bonjour

merci pour les remarques.

[...]

Donc, en gros, pour sortir un entier, j'écrirai
quelque chose du genre (en supposant des int de 32 bits) :

madStream&
operator<( madStream& dest, int value )
{
madStream::sentry sentry( dest ) ;
if ( sentry ) {
streambuf* sb = dest.rdbuf() ;
unsigned tmp = value ;
int shift = 32 ;
while ( dest && shift > 0 ) {


plutot : while (sentry && shilft>0) ?

if ( sb->sputc( (tmp >> shift) & 0xff ) == EOF ) {
dest.clear( std::ios::badbit ) ;
}
shift -= 8 ;
}
}
return dest ;
}

Ça, c'est pour écrire des int comme 4 octets, poids fort d'abord, format
complément à deux. C-à-d format Internet.

Dans un premier temps au moins, madStream::sentry serait assez simple ;
quelque chose du genre :

class madStream::sentry
{
public:
sentry( madStream& dest )
: myDest( dest )
{
}
operator bool() const
{
return ! myDest.fail() ;
}

private:
madStream& myDest ;
}



Ce serait cette classe qui specialiserait le type d'erreur, et (par exemple)
les messages a afficher en consequence ? Je ne vois pas vraiment son utilité
par rapport à un méthode madStream::is_valid() ou quelque chose dans le
genre qui me parrait plus simple.

|> madStream& operator>>(madStream& ims, CMad& rMad)

Comme dans les iostream, je définirais d'abord un istream et un
ostream ; si j'ai besoin des flux bidirectionnels, j'ajouterais un
iostream dérivé des deux (mais dans l'ensemble, je préfèrerais éviter la
bidirectionnalité). Et si on prévoit la bidirectionnalité, il faut
dériver virtuellement de std::ios.

|> {
|> rMad.m_value = ims.rdbuf()->sgetc();
|> return ims;
|> }

Ici aussi, tu ne lis qu'un seul caractère. Pire, tu ne signales aucune
erreur. Comme ci-dessus :

iMadStream&
operator>>( IMadStream& source, int& value )
{
IMadStream::sentry sentry( source ) ;
if ( sentry ) {
streambuf* sb = source.rdbuf() ;
unsigned result = 0 ;
int shift = 32 ;
int tmp = sb->sgetc() ;
while ( tmp != EOF && shift > 0 ) {
shift -= 8 ;
result = result | (tmp << shift) ;
tmp = sb->sgetc() ;


En fait, il me semble que sbumpc est plus adapté a ce que l'on veut faire.

}
if ( shift != 0 ) {
if ( shift == 32 ) {
source.clear( std::ios::eofbit | std::ios::failbit ) ;
} else {
source.clear( std::ios::eofbit | std::ios::badbit ) ;
}
} else {
value = tmp ;
}
}
return source ;

Ici, j'avoue ne pas suivre les conventions des istream standard, mais je
ne sais pas comment faire autrement pour permettre à l'utilisateur de
distinguer entre une vraie fin de fichier, et une erreur de formattage.
(Mais beaucoup dépend du format binaire qu'on implémente, et comment on
sait s'il y a un entier qui suit ou non.)

Aussi, l'affectation de value à la fin n'est pas garantie par la norme.
Mais ça fonctionne avec toutes les implémentations modernes, et je ne
sais pas de tête comment l'écrire d'une façon portable.


Je ne comprend pas. En quoi n'est-ce pas spécifié par la norme.

[...]

|> La chose qui me gene est que j'aurai souhaité cacher le fait que le
|> filebuf doive etre ouvert en mode binaire.

La solution « consacrée », encore, c'est de se modéler sur les iostream.
Tu crées une classe dérivée, fMadStream (ou plutôt ifMadStream et
ofMadStream) qui s'occupe de la gestion du filebuf. Quelque chose du
genre :

class ifMadStream : private virtual std::filebuf, public iMadStream
...

L'astuce avec l'héritage virtuel, c'est un truc que je dois à Dietmar
Kühl -- il s'assure que le filebuf est bien initialisé avant de passer
son adresse au constructeur de iMadStream. Y compris dans le cas où on
dérive un fMadStream bidirectionnel par la suite.


Si j'ai bien compris, le mot clef virtuel que tu utilises n'a qu'un unique
but, appeller le constructeur par defaut de la classe std::filebuf avant le
constructeur de iMadStream ? Est-ce une astuce qui fonctionne car c'était
prévu dans la norme ou bien est-ce un effet de bord du comportement des
compilateurs.
Dans la doc que j'ai, je vois que l'utilisation de l'héritage virtuel est
utilisé pour assurer l'unicité de la construction d'un objet dans le cas
d'un multi-héritage avec plusieurs classes peres identiques. Ai-je mal
compris cette partie ?

Merci

--
Jérémie

Avatar
kanze
"Jeremie Fouche" wrote in message
news:<ccjau9$8vo$...

James Kanze a écrit dans le message :


[...]

Donc, en gros, pour sortir un entier, j'écrirai
quelque chose du genre (en supposant des int de 32 bits) :

madStream&
operator<( madStream& dest, int value )
{
madStream::sentry sentry( dest ) ;
if ( sentry ) {
streambuf* sb = dest.rdbuf() ;
unsigned tmp = value ;
int shift = 32 ;
while ( dest && shift > 0 ) {


plutot : while (sentry && shilft>0) ?


Non. Ça serait logique, peut-être, mais la spécification de
ostream::sentry, c'est que pris comme un booléen, il représente le fait
que l'initialisation pour les sorties s'est bien passée. Il ne suit pas
l'évolution de l'ostream par la suite.

En fait, je ne sais pas si ce ne serait pas plus propre d'ajouter une
variable booléenne, du genre « errorSeen ». Mais vue que si on détecte
une erreur, il faut bien le signaler à travers dest avant de revenir,
autant le faire tout de suite. Et puisqu'en dehors d'un operator<<,
c'est idiomatique que « dest » signifie pas d'erreurs jusqu'ici,
pourquoi pas dans l'operator<< aussi.

if ( sb->sputc( (tmp >> shift) & 0xff ) == EOF ) {
dest.clear( std::ios::badbit ) ;
}
shift -= 8 ;
}
}
return dest ;
}

Ça, c'est pour écrire des int comme 4 octets, poids fort d'abord, format
complément à deux. C-à-d format Internet.

Dans un premier temps au moins, madStream::sentry serait assez simple ;
quelque chose du genre :

class madStream::sentry
{
public:
sentry( madStream& dest )
: myDest( dest )
{
}
operator bool() const
{
return ! myDest.fail() ;
}

private:
madStream& myDest ;
}


Ce serait cette classe qui specialiserait le type d'erreur, et (par
exemple) les messages a afficher en consequence ?


Non. Cette classe sert de sentinelle autour des entrées/sorties. Elle
s'assure que tout est bien initialisé pour les ES -- dans le cas des
entrées formattées textuellement, par exemple, elle saute les blancs.

Je ne vois pas vraiment son utilité par rapport à un méthode
madStream::is_valid() ou quelque chose dans le genre qui me parrait
plus simple.


L'intérêt, c'est de fournir un point avant et après les entées/sorties
où madStream peut intervenir, s'il le faut. Historiquement, il y avait
des fonctions membres opfx() et osfx() (OutputPreFiX et OutputSufFix),
que l'operator<< devait appeler au début et à la fin. L'utilisation
d'une classe rend l'appel à la fin automatique, y compris dans le cas où
on sort de l'operator<< par une exception.

Dans la pratique, je crois que osfx() a toujours été vide. Mais les
auteurs de iostream n'ont pas voulu rénoncer à la possibilité.

Dans ton cas, la seule raison réele de passer par une classe, c'est que
c'est idiomatique. Quiconque qui écrit un operator<< s'attend à
commencer par la construction d'un istream::sentry, puis le vérification
de sa valeur. Faire autrement sème la confusion.

|> madStream& operator>>(madStream& ims, CMad& rMad)

Comme dans les iostream, je définirais d'abord un istream et un
ostream ; si j'ai besoin des flux bidirectionnels, j'ajouterais un
iostream dérivé des deux (mais dans l'ensemble, je préfèrerais
éviter la bidirectionnalité). Et si on prévoit la bidirectionnalité,
il faut dériver virtuellement de std::ios.

|> {
|> rMad.m_value = ims.rdbuf()->sgetc();
|> return ims;
|> }

Ici aussi, tu ne lis qu'un seul caractère. Pire, tu ne signales aucune
erreur. Comme ci-dessus :

iMadStream&
operator>>( IMadStream& source, int& value )
{
IMadStream::sentry sentry( source ) ;
if ( sentry ) {
streambuf* sb = source.rdbuf() ;
unsigned result = 0 ;
int shift = 32 ;
int tmp = sb->sgetc() ;
while ( tmp != EOF && shift > 0 ) {
shift -= 8 ;
result = result | (tmp << shift) ;
tmp = sb->sgetc() ;


En fait, il me semble que sbumpc est plus adapté a ce que l'on veut faire.


Ooops. Tout à fait. Même après tant de temps, je me laisse berner par le
nom.

}
if ( shift != 0 ) {
if ( shift == 32 ) {
source.clear( std::ios::eofbit | std::ios::failbit ) ;
} else {
source.clear( std::ios::eofbit | std::ios::badbit ) ;
}
} else {
value = tmp ;
}
}
return source ;

Ici, j'avoue ne pas suivre les conventions des istream standard,
mais je ne sais pas comment faire autrement pour permettre à
l'utilisateur de distinguer entre une vraie fin de fichier, et une
erreur de formattage. (Mais beaucoup dépend du format binaire qu'on
implémente, et comment on sait s'il y a un entier qui suit ou non.)

Aussi, l'affectation de value à la fin n'est pas garantie par la
norme. Mais ça fonctionne avec toutes les implémentations modernes,
et je ne sais pas de tête comment l'écrire d'une façon portable.


Je ne comprend pas. En quoi n'est-ce pas spécifié par la norme.


L'état de la variable qu'on lit en cas d'erreur. Je crois que c'est
spécifié pour certains types, que la variable n'est pas modifiée. Mais
je ne suis pas sûr que ce soit le cas pour tous les types. Si je lis
vers un std::string, par exemple ? Est-ce que l'implémentation est
obligée à stocker dans une chaîne locale jusqu'à ce qu'elle a fini, pour
être sûr qu'il n'y a pas d'erreur, et puis affecter la chaîne à sa
destination finale, ou est-ce que l'implémentation peut stocker
directement dans la destination finale ?

[...]

|> La chose qui me gene est que j'aurai souhaité cacher le fait que
|> le filebuf doive etre ouvert en mode binaire.

La solution « consacrée », encore, c'est de se modéler sur les
iostream. Tu crées une classe dérivée, fMadStream (ou plutôt
ifMadStream et ofMadStream) qui s'occupe de la gestion du
filebuf. Quelque chose du genre :

class ifMadStream : private virtual std::filebuf, public iMadStream
...

L'astuce avec l'héritage virtuel, c'est un truc que je dois à
Dietmar Kühl -- il s'assure que le filebuf est bien initialisé avant
de passer son adresse au constructeur de iMadStream. Y compris dans
le cas où on dérive un fMadStream bidirectionnel par la suite.


Si j'ai bien compris, le mot clef virtuel que tu utilises n'a qu'un
unique but, appeller le constructeur par defaut de la classe
std::filebuf avant le constructeur de iMadStream ?


Dans ce cas-ci. En général, il assure qu'il n'a qu'une seule instance de
la classe dans tout l'hièrarchie. Seulement, s'il n'y a qu'une seule
instance, or que plusieurs classes en dérivent, on ne sait pas qui est
maître, et qui doit l'initialiser. Pour résoudre ce problème, la règle
est que les bases virtuelles seront initialisées par la classe la plus
dérivée, avant toutes les autres bases.

Est-ce une astuce qui fonctionne car c'était prévu dans la norme ou
bien est-ce un effet de bord du comportement des compilateurs.


C'est garantie par la norme.

Dans la doc que j'ai, je vois que l'utilisation de l'héritage virtuel
est utilisé pour assurer l'unicité de la construction d'un objet dans
le cas d'un multi-héritage avec plusieurs classes peres
identiques. Ai-je mal compris cette partie ?


Pas du tout. La garantie que la base virtuelle soit initialisée d'abord
est un effet de bord de cette garantie.

En fait, ici, autant qu'on ne dérive pas de ifMadStream, le virtuel
n'est pas nécessaire non plus, parce que les classes de base sont
initialisées dans l'ordre de leur déclaration. Mais on pourrait bien
imaginer dériver de ifMadStream, par exemple, un fMadStream qui dérive à
la fois de ifMadStream et de ofMadStream. Dans ce cas-là, la dérivation
virtuelle devient importante.

--
James Kanze GABI Software http://www.gabi-soft.fr
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
kanze
"Jeremie Fouche" wrote in message
news:<ccjau9$8vo$...

James Kanze a écrit dans le message :


[...]

Donc, en gros, pour sortir un entier, j'écrirai
quelque chose du genre (en supposant des int de 32 bits) :

madStream&
operator<( madStream& dest, int value )
{
madStream::sentry sentry( dest ) ;
if ( sentry ) {
streambuf* sb = dest.rdbuf() ;
unsigned tmp = value ;
int shift = 32 ;
while ( dest && shift > 0 ) {


plutot : while (sentry && shilft>0) ?


Non. Ça serait logique, peut-être, mais la spécification de
ostream::sentry, c'est que pris comme un booléen, il représente le fait
que l'initialisation pour les sorties s'est bien passée. Il ne suit pas
l'évolution de l'ostream par la suite.

En fait, je ne sais pas si ce ne serait pas plus propre d'ajouter une
variable booléenne, du genre « errorSeen ». Mais vue que si on détecte
une erreur, il faut bien le signaler à travers dest avant de revenir,
autant le faire tout de suite. Et puisqu'en dehors d'un operator<<,
c'est idiomatique que « dest » signifie pas d'erreurs jusqu'ici,
pourquoi pas dans l'operator<< aussi.

if ( sb->sputc( (tmp >> shift) & 0xff ) == EOF ) {
dest.clear( std::ios::badbit ) ;
}
shift -= 8 ;
}
}
return dest ;
}

Ça, c'est pour écrire des int comme 4 octets, poids fort d'abord, format
complément à deux. C-à-d format Internet.

Dans un premier temps au moins, madStream::sentry serait assez simple ;
quelque chose du genre :

class madStream::sentry
{
public:
sentry( madStream& dest )
: myDest( dest )
{
}
operator bool() const
{
return ! myDest.fail() ;
}

private:
madStream& myDest ;
}


Ce serait cette classe qui specialiserait le type d'erreur, et (par
exemple) les messages a afficher en consequence ?


Non. Cette classe sert de sentinelle autour des entrées/sorties. Elle
s'assure que tout est bien initialisé pour les ES -- dans le cas des
entrées formattées textuellement, par exemple, elle saute les blancs.

Je ne vois pas vraiment son utilité par rapport à un méthode
madStream::is_valid() ou quelque chose dans le genre qui me parrait
plus simple.


L'intérêt, c'est de fournir un point avant et après les entées/sorties
où madStream peut intervenir, s'il le faut. Historiquement, il y avait
des fonctions membres opfx() et osfx() (OutputPreFiX et OutputSufFix),
que l'operator<< devait appeler au début et à la fin. L'utilisation
d'une classe rend l'appel à la fin automatique, y compris dans le cas où
on sort de l'operator<< par une exception.

Dans la pratique, je crois que osfx() a toujours été vide. Mais les
auteurs de iostream n'ont pas voulu rénoncer à la possibilité.

Dans ton cas, la seule raison réele de passer par une classe, c'est que
c'est idiomatique. Quiconque qui écrit un operator<< s'attend à
commencer par la construction d'un istream::sentry, puis le vérification
de sa valeur. Faire autrement sème la confusion.

|> madStream& operator>>(madStream& ims, CMad& rMad)

Comme dans les iostream, je définirais d'abord un istream et un
ostream ; si j'ai besoin des flux bidirectionnels, j'ajouterais un
iostream dérivé des deux (mais dans l'ensemble, je préfèrerais
éviter la bidirectionnalité). Et si on prévoit la bidirectionnalité,
il faut dériver virtuellement de std::ios.

|> {
|> rMad.m_value = ims.rdbuf()->sgetc();
|> return ims;
|> }

Ici aussi, tu ne lis qu'un seul caractère. Pire, tu ne signales aucune
erreur. Comme ci-dessus :

iMadStream&
operator>>( IMadStream& source, int& value )
{
IMadStream::sentry sentry( source ) ;
if ( sentry ) {
streambuf* sb = source.rdbuf() ;
unsigned result = 0 ;
int shift = 32 ;
int tmp = sb->sgetc() ;
while ( tmp != EOF && shift > 0 ) {
shift -= 8 ;
result = result | (tmp << shift) ;
tmp = sb->sgetc() ;


En fait, il me semble que sbumpc est plus adapté a ce que l'on veut faire.


Ooops. Tout à fait. Même après tant de temps, je me laisse berner par le
nom.

}
if ( shift != 0 ) {
if ( shift == 32 ) {
source.clear( std::ios::eofbit | std::ios::failbit ) ;
} else {
source.clear( std::ios::eofbit | std::ios::badbit ) ;
}
} else {
value = tmp ;
}
}
return source ;

Ici, j'avoue ne pas suivre les conventions des istream standard,
mais je ne sais pas comment faire autrement pour permettre à
l'utilisateur de distinguer entre une vraie fin de fichier, et une
erreur de formattage. (Mais beaucoup dépend du format binaire qu'on
implémente, et comment on sait s'il y a un entier qui suit ou non.)

Aussi, l'affectation de value à la fin n'est pas garantie par la
norme. Mais ça fonctionne avec toutes les implémentations modernes,
et je ne sais pas de tête comment l'écrire d'une façon portable.


Je ne comprend pas. En quoi n'est-ce pas spécifié par la norme.


L'état de la variable qu'on lit en cas d'erreur. Je crois que c'est
spécifié pour certains types, que la variable n'est pas modifiée. Mais
je ne suis pas sûr que ce soit le cas pour tous les types. Si je lis
vers un std::string, par exemple ? Est-ce que l'implémentation est
obligée à stocker dans une chaîne locale jusqu'à ce qu'elle a fini, pour
être sûr qu'il n'y a pas d'erreur, et puis affecter la chaîne à sa
destination finale, ou est-ce que l'implémentation peut stocker
directement dans la destination finale ?

[...]

|> La chose qui me gene est que j'aurai souhaité cacher le fait que
|> le filebuf doive etre ouvert en mode binaire.

La solution « consacrée », encore, c'est de se modéler sur les
iostream. Tu crées une classe dérivée, fMadStream (ou plutôt
ifMadStream et ofMadStream) qui s'occupe de la gestion du
filebuf. Quelque chose du genre :

class ifMadStream : private virtual std::filebuf, public iMadStream
...

L'astuce avec l'héritage virtuel, c'est un truc que je dois à
Dietmar Kühl -- il s'assure que le filebuf est bien initialisé avant
de passer son adresse au constructeur de iMadStream. Y compris dans
le cas où on dérive un fMadStream bidirectionnel par la suite.


Si j'ai bien compris, le mot clef virtuel que tu utilises n'a qu'un
unique but, appeller le constructeur par defaut de la classe
std::filebuf avant le constructeur de iMadStream ?


Dans ce cas-ci. En général, il assure qu'il n'a qu'une seule instance de
la classe dans tout l'hièrarchie. Seulement, s'il n'y a qu'une seule
instance, or que plusieurs classes en dérivent, on ne sait pas qui est
maître, et qui doit l'initialiser. Pour résoudre ce problème, la règle
est que les bases virtuelles seront initialisées par la classe la plus
dérivée, avant toutes les autres bases.

Est-ce une astuce qui fonctionne car c'était prévu dans la norme ou
bien est-ce un effet de bord du comportement des compilateurs.


C'est garantie par la norme.

Dans la doc que j'ai, je vois que l'utilisation de l'héritage virtuel
est utilisé pour assurer l'unicité de la construction d'un objet dans
le cas d'un multi-héritage avec plusieurs classes peres
identiques. Ai-je mal compris cette partie ?


Pas du tout. La garantie que la base virtuelle soit initialisée d'abord
est un effet de bord de cette garantie.

En fait, ici, autant qu'on ne dérive pas de ifMadStream, le virtuel
n'est pas nécessaire non plus, parce que les classes de base sont
initialisées dans l'ordre de leur déclaration. Mais on pourrait bien
imaginer dériver de ifMadStream, par exemple, un fMadStream qui dérive à
la fois de ifMadStream et de ofMadStream. Dans ce cas-là, la dérivation
virtuelle devient importante.

--
James Kanze GABI Software http://www.gabi-soft.fr
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
drkm
writes:

"Jeremie Fouche" wrote in message
news:<ccjau9$8vo$...

James Kanze a écrit dans le message :




[...]

tmp = sb->sgetc() ;


En fait, il me semble que sbumpc est plus adapté a ce que l'on veut
faire.


Ooops. Tout à fait. Même après tant de temps, je me laisse berner
par le nom.


Si je comprend bien, l'unique différence est que sgetc() fait un
read ahead, il lit le prochain caractère sans incrémenter la position
courante ?

[...]

Je ne comprend pas. En quoi n'est-ce pas spécifié par la norme.


L'état de la variable qu'on lit en cas d'erreur. Je crois que c'est
spécifié pour certains types, que la variable n'est pas modifiée. Mais
je ne suis pas sûr que ce soit le cas pour tous les types. Si je lis
vers un std::string, par exemple ? Est-ce que l'implémentation est
obligée à stocker dans une chaîne locale jusqu'à ce qu'elle a fini, pour
être sûr qu'il n'y a pas d'erreur, et puis affecter la chaîne à sa
destination finale, ou est-ce que l'implémentation peut stocker
directement dans la destination finale ?


Je ne sais pas si la norme le spécifie, mais je n'y vois pas
d'opposition. Je ne vois pas vraiment d'intérêt de modifier
directement la chaîne cible par rapport à

std::string result ;
// remplissage de result ...
target.swap( result ) ;

Je vois par contre des inconvénients. Je vais peut-être jeter un coup
d'oeil dans la norme, à la recherche de cette (absence de ?) garantie.

Sinon, quelqu'un voit-il une raison de ne pas garantir l'atomicité
de la lecture d'une chaîne ? Le code ci-dessus n'est-il pas assez bon
(pour une définition ad-hoc de bon) pour garantir cette atomicité ?

[...]

--drkm



Avatar
drkm
drkm writes:

Je vais peut-être jeter un coup
d'oeil dans la norme, à la recherche de cette (absence de ?) garantie.


21.3.7.9 Inserters and extractors [lib.string.io]

template<class charT, class traits, class Allocator>
basic_istream<charT,traits>&
operator>>(basic_istream<charT,traits>& is,
basic_string<charT,traits,Allocator>& str);

1. Effects: Begins by constructing a sentry object k as if k were
constructed by typename basic_istream<charT,traits>::sentry
k(is). If bool(k) is true, it calls str.erase() and then
extracts characters from is and appends them to str as if by
calling str.append(1,c). If is.width() [...]

L'opérateur d'entrée commence donc par appeler erase() sur la
chaîne. Pourquoi une telle contrainte ? Pourquoi ne pas imposer
plutôt l'atomicité de la leture ? Quelque chose comme :

template< class charT , class traits , class Allocator >
basic_istream< charT , traits > &
operator>>(
basic_istream< charT , traits > & is ,
basic_string< charT , traits , Allocator > & str
)
{
typename basic_istream< charT , traits >::sentry sentry( is ) ;
if ( sentry ) {
basic_string< charT , traits , Allocator > result ;
// ... remplissage de result ...
str.swap( result ) ;
}
return is ;
}

n'est-il pas plus satisfaisant ? J'imagine que j'ai loupé quelque
chose, mais je n'arrive pas à voir la motivation derrière « If bool(k)
is true, _it calls str.erase()_ and then [...] ».

Quelqu'un peut-il m'éclairer ?

--drkm

Avatar
kanze
drkm wrote in message
news:...
writes:

"Jeremie Fouche" wrote in message
news:<ccjau9$8vo$...

James Kanze a écrit dans le message :




[...]

tmp = sb->sgetc() ;


En fait, il me semble que sbumpc est plus adapté a ce que l'on veut
faire.


Ooops. Tout à fait. Même après tant de temps, je me laisse berner
par le nom.


Si je comprend bien, l'unique différence est que sgetc() fait un
read ahead, il lit le prochain caractère sans incrémenter la position
courante ?


Si on assimile la position dans le flux à un pointeur p, on a les
équivalences suivantes :

*p sgetc()
*p++ sbumpc()
*++p snextc()

La confusion vient du fait que dans istream, on a :

*p peek()
*p ++ get()

Plus généralement, j'ai toujours vu les noms de istream pour ce genre de
chose.

[...]

Je ne comprend pas. En quoi n'est-ce pas spécifié par la norme.


L'état de la variable qu'on lit en cas d'erreur. Je crois que c'est
spécifié pour certains types, que la variable n'est pas
modifiée. Mais je ne suis pas sûr que ce soit le cas pour tous les
types. Si je lis vers un std::string, par exemple ? Est-ce que
l'implémentation est obligée à stocker dans une chaîne locale
jusqu'à ce qu'elle a fini, pour être sûr qu'il n'y a pas d'erreur,
et puis affecter la chaîne à sa destination finale, ou est-ce que
l'implémentation peut stocker directement dans la destination
finale ?


Je ne sais pas si la norme le spécifie, mais je n'y vois pas
d'opposition. Je ne vois pas vraiment d'intérêt de modifier
directement la chaîne cible par rapport à

std::string result ;
// remplissage de result ...
target.swap( result ) ;

Je vois par contre des inconvénients. Je vais peut-être jeter un coup
d'oeil dans la norme, à la recherche de cette (absence de ?) garantie.

Sinon, quelqu'un voit-il une raison de ne pas garantir l'atomicité
de la lecture d'une chaîne ? Le code ci-dessus n'est-il pas assez bon
(pour une définition ad-hoc de bon) pour garantir cette atomicité ?


Je viens de régarder. La norme est claire -- au moins pour >> et getline
vers un std::string, l'implémentation est obligée à travailler sur la
chaîne de l'utilisateur.

L'intérêt, éventuellement, c'est que même en cas d'erreur, l'utilisateur
peut accéder aux caractères réelement lus. Donc, par exemple, si on lit
un fichier ligne par ligne avec getline, et la dernière ligne n'est pas
terminée par un 'n', on a une erreur (qu'on ne peut pas distinguer
d'une vraie fin de fichier au moyen des bits d'état), mais on a quand
même les caractères lus. On pourrait donc imaginer quelque chose du
genre :

std::string ligne ;
while ( getline( fichier, ligne ) {
traite( ligne ) ;
}
if ( ! ligne.empty() ) {
ProgramStatus::instance().setError( ProgramStatus:: warning )
<< "'n' manque en fin de fichier" ;
traite( ligne ) ;
}

(Dans la pratique, en tout cas. Parce qu'on ne modifie ligne que si le
sentry est vrai. Ce qui en limite l'utilité -- ça doit marcher si
l'accès précédant est getline, mais pas forcement dans d'autres cas.)

--
James Kanze GABI Software http://www.gabi-soft.fr
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
kanze
drkm wrote in message
news:...
drkm writes:

Je vais peut-être jeter un
coup d'oeil dans la norme, à la recherche de cette (absence de ?)
garantie.


21.3.7.9 Inserters and extractors [lib.string.io]

template<class charT, class traits, class Allocator>
basic_istream<charT,traits>&
operator>>(basic_istream<charT,traits>& is,
basic_string<charT,traits,Allocator>& str);

1. Effects: Begins by constructing a sentry object k as if k were
constructed by typename basic_istream<charT,traits>::sentry
k(is). If bool(k) is true, it calls str.erase() and then
extracts characters from is and appends them to str as if by
calling str.append(1,c). If is.width() [...]

L'opérateur d'entrée commence donc par appeler erase() sur la
chaîne.


Pas tout à fait. L'opérateur d'entrée commence par tester le sentry. Si
la lecture échoue déjà lors de ce test, la chaîne n'est pas modifiée.

istringstream in( "123n" ) ;
int i ;
in >> i ;
string s( "bidon" ) ;
in >> s ;

La norme exige que s contient encore "bidon". Si en revanche tu avais

istringstream in( "123n" ) ;
string l ;
getline( in, l ) ;
string s( "bidon" ) ;
in >> s ;

s doit bien être vidée.

Sympa, non ?

Pourquoi une telle contrainte ?


La sur-spécification ? Comme j'ai indiqué ailleurs, j'en vois bien un
intérêt dans le cas de getline -- dans le cas où il manque un 'n' à la
fin de fichier, c'est la seule façon à accéder aux caractères de la
dernière ligne.

Note que dans le cas des entrées numériques (ou vers bool), la norme
exige que dans le cas d'erreur, la valeur ne soit pas modifiée. Il y a
donc incohérence.

Pourquoi ne pas imposer plutôt l'atomicité de la leture ?


Pourquoi imposer quoique ce soit, dans le cas où il n'y a pas une
utilité certaine ? Dans le cas de getline, c'est utile ; ça serait même
plus utile si on imposait le vidage de la chaîne *avant* la construction
de sentry. Dans les autres cas, j'ai mes doutes sur l'intérêt.

Si on impose le vidage d'une chaîne, ou une mise à jour quelconque, il
serait plus utile de l'exiger *avant* la construction de sentry, ou au
moins aussi dans le cas où le sentry est faux.

Quelque chose comme :

template< class charT , class traits , class Allocator >
basic_istream< charT , traits > &
operator>>(
basic_istream< charT , traits > & is ,
basic_string< charT , traits , Allocator > & str
)
{
typename basic_istream< charT , traits >::sentry sentry( is ) ;
if ( sentry ) {
basic_string< charT , traits , Allocator > result ;
// ... remplissage de result ...
str.swap( result ) ;
}
return is ;
}

n'est-il pas plus satisfaisant ?


Intellectuellement, oui. Dans le cas de getline, au moins, c'est moins
utile sur le plan pratique.

J'imagine que j'ai loupé quelque
chose, mais je n'arrive pas à voir la motivation derrière « If bool(k)
is true, _it calls str.erase()_ and then [...] ».


Introduire des dépendances sur les entrées précédantes, pour rendre la
programmation plus aléatoire ? :-)

--
James Kanze GABI Software http://www.gabi-soft.fr
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


1 2