OVH Cloud OVH Cloud

Champ de bits type safe

33 réponses
Avatar
Guillaume GOURDIN
Bonjour à tous,

il est assez commun je crois d'utiliser des champs de bits, du style

const int PROPERTY1 = 1<<0;
const int PROPERTY2 = 1<<1;
const int PROPERTY3 = 1<<2;
const int PROPERTY4 = 1<<3;

class item
{
void set_property(int property);
}

item i;
i.set_properties(PROPERTY1|PROPERTY2|PROPERTY4);

La chose qui me dérange c'est que tout ça n'est pas très type safe, et
rien n'empêche de taper par exemple 'i.set_properties(RAND_MAX)'.

Ma question est donc: quelles sont les solutions que vous avez
développées pour rendre ce genre de choses plus robustes?

Merci pour vos réflexions.

- Guillaume -

10 réponses

1 2 3 4
Avatar
Falk Tannhäuser
Guillaume GOURDIN schrieb:
il est assez commun je crois d'utiliser des champs de bits, du style

const int PROPERTY1 = 1<<0;
const int PROPERTY2 = 1<<1;
const int PROPERTY3 = 1<<2;
const int PROPERTY4 = 1<<3;

class item
{
void set_property(int property);
}

item i;
i.set_properties(PROPERTY1|PROPERTY2|PROPERTY4);

La chose qui me dérange c'est que tout ça n'est pas très type safe, et
rien n'empêche de taper par exemple 'i.set_properties(RAND_MAX)'.

Ma question est donc: quelles sont les solutions que vous avez
développées pour rendre ce genre de choses plus robustes?



Le plus simple me semble de créer un type énuméré, puis de surcharger les opérateurs dont on a besoin afin qu'ils renvoient le
bon type :


enum Properties
{
PROPERTY1 = 1<<0,
PROPERTY2 = 1<<1,
PROPERTY3 = 1<<2,
PROPERTY4 = 1<<3
};

inline Properties operator|(Properties a, Properties b)
{ // Note: Unary '+' forces integral promotion
return static_cast<Properties>(+a | +b);
}

class Item
{
public:
void set_property(Properties property);
};


Ainsi, on ne peut plus passer un 'int' à Item::set_property() sans cast.
Falk
Avatar
Fabien LE LEZ
On Mon, 30 Mar 2009 19:39:15 +0000 (UTC), Marc
:

C'est peut-être un peu strict comme réponse. On peut créer une classe
propriété (qui contient un int), définir un operateur | sur les objets de
cette classe, contrôler les constructeurs pour que toute propriété soit
soit une des 4 de base soit une copie soit le résultat de |.



En gros, tu proposes une solution compliquée pour faire fonctionner un
abus, alors qu'il existe une solution simple et légitime au problème.
Avatar
Doms
Bonjour,

Plus sérieusement, stocker un entier dans un "int", ça ne prémunit pas
contre les erreurs, mais au moins ça fait sens.
Stocker plusieurs booléens dans un "int", c'est un abus pur et simple.
Pour stocker plusieurs booléens, on utilise plusieurs "bool", qu'on
peut regrouper dans une classe si ça se justifie.



Mouai, notons que dans nombre de cas, il est utile d'utiliser
1 bit pour 1 boolen, genre quand on code pour des truc embedded
qui n'ont pas autant de memoire qu'on le souhaiterait. Reste que sur
1 octet, on peut stocker de manière licite (à mon avis, hein) 8 valeurs
booleennes.

Pourquoi ne pas utiliser pour cela des vrai champs de bits ou
le compilo fait le travail et qui se débrouille tout seul :
struct sr
{
unsigned int MonBool1 : 1;
unsigned int MonBool2 : 1;
unsigned int MonBool3 : 1;
unsigned int MonBool4 : 1;
unsigned int MonBool5 : 1;
unsigned int MonBool6 : 1;
unsigned int MonBool7 : 1;
unsigned int MonBool8 : 1;
};

C'est licite en C++, non ? Ca l'était en C et le compilo gérait lui même toute la mécanique
d'accès...

mes 2,5 euros. J'ai pas testé en C++ si les compilos acceptent ça encore.

Doms.
Avatar
Michel Decima
Fabien LE LEZ a écrit :
On Mon, 30 Mar 2009 19:39:15 +0000 (UTC), Marc
:

C'est peut-être un peu strict comme réponse. On peut créer une classe
propriété (qui contient un int), définir un operateur | sur les objets de
cette classe, contrôler les constructeurs pour que toute propriété soit
soit une des 4 de base soit une copie soit le résultat de |.



En gros, tu proposes une solution compliquée pour faire fonctionner un
abus, alors qu'il existe une solution simple et légitime au problème.



Cette solution compliquee est utilisee dans l'implementation de la
bibliotheque standard de g++ pour les options de fmtflags, iostate,
openmode de std::ios_base. Plus precisement, ce n'est pas une classe,
mais un enum, avec la redefinition des operateurs necessaires.

D'apres le commentaire qui precede ces declarations, l'objectif est
une meilleure securite sur les types lors des appels de fonctions.
(Il est aussi note que les expressions formees ne sont plus des
'compile-time constants').

J'ai donne l'exemple seulement pour montrer que la "solution compliquee"
est utilisable, en particulier quand l'usage des operateurs bit à bit
est imposé (ici par le standard).
Avatar
Michel Decima
Falk Tannhäuser a écrit :

enum Properties
{
PROPERTY1 = 1<<0,
PROPERTY2 = 1<<1,
PROPERTY3 = 1<<2,
PROPERTY4 = 1<<3
};

inline Properties operator|(Properties a, Properties b)
{ // Note: Unary '+' forces integral promotion
return static_cast<Properties>(+a | +b);
}



Ca ne serait pas plus explicite d'utiliser static_cast<int> pour
la conversion en entier ? J'avoue que sans le commentaire, je
n'aurais pas compris tout de suite...
Avatar
James Kanze
On Mar 30, 4:03 pm, Fabien LE LEZ wrote:
On Mon, 30 Mar 2009 15:13:55 +0200, Guillaume GOURDIN
:



>il est assez commun je crois d'utiliser des champs de bits,
>du style



>const int PROPERTY1 = 1<<0;
>const int PROPERTY2 = 1<<1;
>const int PROPERTY3 = 1<<2;



Bof...



>La chose qui me dérange c'est que tout ça n'est pas très type
>safe



Yep.



Il me paraît plus sûr de rassembler les paramètres dans une
classe :



struct Param
{
bool property1;
bool property2;
bool property3;
};



Ça dépend de la sémantique des properties. Pour des choses comme
std::ios_base::fmtflag, ça serait plutôt pénible, je crois.

L'idiome plus ou moins consacré, c'est d'utiliser un enum, avec
surcharge des opérateurs :

enum Properties
{
property1 = 0x01 ,
property2 = 0x02 ,
property3 = 0x04 ,
// ...
} ;

Properties
operator|(
Properties lhs,
Properties rhs )
{
return static_cast< Properties >(
static_cast< unsigned >( lhs )
| static_cast< unsigned >( rhs ) ) ;
}
// ...

Comme on voit, ce n'est pas particulièrement joli, mais une fois
les opératateurs définis, ça marche pour la plupart des choses.
Reste que le résultat de l'ou n'est pas une expression constante
(mais si j'ai bien compris, il pourrait l'être avec la nouvelle
norme). Et ça n'empêche pas des conneries du genre
static_cast< Properties >( RAND_MAX ). Mais au moins, il faut
le faire exprès.

Au niveau de l'implémentation, il faut bien choisir le type
entier -- si l'enum a plus de bits qu'un unsigned, la
conversion va causer une perte d'information. (Il existe des
techniques pour déterminer le type automatiquement, avec des
templates. Mais franchement, c'est une chose qui ne dois servir
que dans du code généré.)

--
James Kanze (GABI Software) email:
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
James Kanze
On Mar 30, 5:26 pm, (Pascal J. Bourguignon)
wrote:
Fabien LE LEZ writes:



> On Mon, 30 Mar 2009 16:41:34 +0200, Alexandre Bacquart
> :



>>Et boycotter l'opérateur | ?



> Pour cette utilisation, oui.



Mais si tu vas par là:



class Patient {
void setTemperature(int temp);
};



comme on ne voudrait pas appeler
p.setTemperature(rand(MAX_INT)); (on voudrait une temperature
entière entre 20 et 46 "type safe"), on va devroir boycotter
C++ complètement puisqu'il ne connait pas les types
intervales.



Mais on peut les implémenter:-). Pas que ça marcherait mieux, si
rand renvoie une valeur dans l'intervale.

Passons à ADA...



Pas forcement une mauvaise idée, à condition de trouver un
compilateur qui va bien, des collègues qui connaissent bien le
langage, etc., etc. Et encore. Le grand avantage de C++, c'est
justement qu'il y a tellement des gens qui s'en servent que les
techniques évoluent plus vites qu'avec d'autres langages.

--
James Kanze (GABI Software) email:
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
Falk Tannhäuser
Michel Decima wrote:
Falk Tannhäuser a écrit :

enum Properties
{
PROPERTY1 = 1<<0,
PROPERTY2 = 1<<1,
PROPERTY3 = 1<<2,
PROPERTY4 = 1<<3
};

inline Properties operator|(Properties a, Properties b)
{ // Note: Unary '+' forces integral promotion
return static_cast<Properties>(+a | +b);
}



Ca ne serait pas plus explicite d'utiliser static_cast<int> pour
la conversion en entier ? J'avoue que sans le commentaire, je
n'aurais pas compris tout de suite...



Je trouve que la solution avec le static_cast<int> a deux défauts : un
mineur (ça fait plus de texte à taper :-)) et un majeur au niveau de la
maintenance - le jour où quelqu'un ajoute un 'PROPERTY42 = 1ULL << 42'
dans l'enum, int ne convient plus et on risque d'oublier de mettre à
jour le cast, tandis qu'avec le +, le compilateur se débrouille tout
seul pour trouver le type intégral qui convient (chaque type énuméré
possède un type intégral sous-jacent, suffisamment grand pour
représenter les valeurs énumérées).

Falk
Avatar
James Kanze
On Mar 30, 7:31 pm, Fabien LE LEZ wrote:
On Mon, 30 Mar 2009 17:26:11 +0200, (Pascal J.
Bourguignon):



>comme on ne voudrait pas appeler p.setTemperature(rand(MAX_INT));



Pourquoi pas ?
rand(MAX_INT) renvoie, si je ne m'abuse, un entier supérieur ou égal à
zéro. Donc, une température physiquement atteignable.



Plus sérieusement, stocker un entier dans un "int", ça ne
prémunit pas contre les erreurs, mais au moins ça fait sens.
Stocker plusieurs booléens dans un "int", c'est un abus pur et
simple. Pour stocker plusieurs booléens, on utilise plusieurs
"bool", qu'on peut regrouper dans une classe si ça se
justifie.



C'est simplement une technique de l'implémentation. Il s'agit
d'un ensemble sur le domaine des Properties. Tu veux qu'il ait
des caractèristiques et l'interface d'un ensemble, avec union,
intersection, etc. Si le nombre d'éléments dans le domaine est
petit, utiliser un bit par élément, pour indiquer son
appartenance à l'ensemble, ce n'est pas forcément une mauvaise
implémentation ; c'est en tout cas quelque chose de classique
(un bitmap).

Je l'utilise dans mon SetOfCharacter, pour des encodages simple.
(Pour l'UTF-8, c'est un trie. Mais les feuilles sont des
bitmaps de 64 bits).

--
James Kanze (GABI Software) email:
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
James Kanze
On Mar 30, 9:23 pm, Fabien LE LEZ wrote:
On Mon, 30 Mar 2009 20:07:41 +0200, Alexandre Bacquart
:



>C'est bien ça qui me dérange par rapport à la question qui était posée.



Guillaume demande en substance : "Je veux stocker des booléens
dans un int, est-ce une bonne idée ?"



Ce n'est pas comme ça que je l'ai compris. Il a démandé comment
implémenter des « champs de bits », ce que je crois (mais je
ne suis pas sûr) l'expression française pour « bitmap ». La
sémantique ciblée, c'est plutôt un ensemble sur un petit
domaine.

Je lui réponds : non. Pour stocker des booléens, il faut
utiliser des "bool". Toute autre solution est purement et
simplement un abus, qui n'offrira alors aucune garantie.



On pourrait, certes, utiliser un tableau ou un struct de bool.
Seulement, les opérations telles l'union ou l'intersection
deviendra beaucoup plus lourd (surtout dans le cas d'un struct).

Je comprends ton point de vue ; un « bit » n'a pas une
sémantique de bool en C++. Mais la structure de données
« bitmap » est assez consacrée et connue que je crois qu'on
pourrait s'en servir. (C'est une des implémentations classiques
des std::ios_base::fmtflags, par exemple. En g++, par exemple,
on a :
enum _Ios_Fmtflags
{
_S_boolalpha = 1L << 0,
_S_dec = 1L << 1,
_S_fixed = 1L << 2,
_S_hex = 1L << 3,
_S_internal = 1L << 4,
_S_left = 1L << 5,
_S_oct = 1L << 6,
_S_right = 1L << 7,
_S_scientific = 1L << 8,
_S_showbase = 1L << 9,
_S_showpoint = 1L << 10,
_S_showpos = 1L << 11,
_S_skipws = 1L << 12,
_S_unitbuf = 1L << 13,
_S_uppercase = 1L << 14,
_S_adjustfield = _S_left | _S_right | _S_internal,
_S_basefield = _S_dec | _S_oct | _S_hex,
_S_floatfield = _S_scientific | _S_fixed,
_S_ios_fmtflags_end = 1L << 16
};

inline _Ios_Fmtflags
operator&(_Ios_Fmtflags __a, _Ios_Fmtflags __b)
{ return _Ios_Fmtflags(static_cast<int>(__a) & static_cast<int>
(__b)); }

inline _Ios_Fmtflags
operator|(_Ios_Fmtflags __a, _Ios_Fmtflags __b)
{ return _Ios_Fmtflags(static_cast<int>(__a) | static_cast<int>
(__b)); }

inline _Ios_Fmtflags
operator^(_Ios_Fmtflags __a, _Ios_Fmtflags __b)
{ return _Ios_Fmtflags(static_cast<int>(__a) ^ static_cast<int>
(__b)); }

inline _Ios_Fmtflags&
operator|=(_Ios_Fmtflags& __a, _Ios_Fmtflags __b)
{ return __a = __a | __b; }

inline _Ios_Fmtflags&
operator&=(_Ios_Fmtflags& __a, _Ios_Fmtflags __b)
{ return __a = __a & __b; }

inline _Ios_Fmtflags&
operator^=(_Ios_Fmtflags& __a, _Ios_Fmtflags __b)
{ return __a = __a ^ __b; }

inline _Ios_Fmtflags
operator~(_Ios_Fmtflags __a)
{ return _Ios_Fmtflags(~static_cast<int>(__a)); }
// ...
typedef _Ios_Fmtflags fmtflags;
.)

--
James Kanze (GABI Software) email:
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 3 4