C++ et memset() | 2 vote(s)
Par Emmanuel Deloget, lundi 9 juillet 2007 à 18:00 :: C++ :: permalien #89
En C++ il est relativement courant d'initialiser une structure avec la terrible fonction std::memset() de la librairie standard. Je vous livre ici mon opinion: c'est Mal, et c'est Dangereux. Et je vais maintenant vous expliquer pourquoi.
Le problème
Soit le prototype de la fonction std::memset()):
namespace std
{
void* memset(void*ptr, int value, size_t count);
}
std::memset() (ou, pour faire plus court, memset()) initialise count octets avec la valeur value à partir de la position mémoire ptr. Le premier point, c'est que malgré le fait que value soit un entier, il s'agit bel et bien d'octets (en fait, de char - ce qui est une notion différente[1]) qui sont écrits en mémoire à l'adresse ptr.
Il semblerait qu'en C, memset() soit reconnu comme étant la manière la plus simple et la plus propre d'initialiser une structure ou un tableau. En C++, il s'agit de la manière la plus abrupte et la plus sale qu'on puisse imaginer d'initialiser une structure ou un tableau.
La raison principale qui me vient à l'esprit est la suivante: qui vous dit que vous être en train d'initialiser une structure POD[2] ? J'ai encore un exemple de cette terrible erreur sous les yeux au moment ou j'écris ce billet :
struct MyStruct
{
int field1: //Ok, POD
char field2; //Ok, POD
short field3; //Ok, POD
SomeComplexType field4; //NOT OK, not POD
};
Et bien évidemment, SomeComplexType possède un pointeur (plusieurs même, puisque dans mon exemple il s'agit de chaines du type std::string) et plus important: une table des fonctions virtuelle. Que notre memset() vient juste d'effacer.
Et oui, memset() efface tout, même les pointeurs vers les vtables - si ce pointeur est stocké dans l'instance de la classe (mais c'est le cas dans la plupart des implémentations du C++ que je connais). Quelque chose va donc un jour prendre un tournant que nous ne souhaitons pas, espérons donc juste que ça ne sera pas pendant une présentation au client.
Ensuite, rien ne vous dit que vous ne vous êtes pas trompé dans l'un des paramètre passé à memset(). Si vous vous trompez dans la taille, votre structure ne sera pas initialisée correctement - ou pire, vous risquer d'écrire dans un zone mémoire ou vous ne souhaitiez pas écrire. Si vous vous trompez dans la valeur à écrire (par exemple, vous spécifiez 300), qui sait ce qui peut advenir par la suite.
Enfin, mais ce n'est pas très important, memset() n'est pas concerné par le type des données que vous initialisé. Il se borne a écrire en mémoire une suite de bytes sans signification. C'est quelque chose que je peux comprendre au niveau programmation, mais au niveau architecture ou maintenance, c'est une horreur. Et puis, qui vous dit que d'avoir tous les champs initialisé à 0 est une initialisation logique pour ce type ? Et s'il était plus logique d'initialiser certains champs booléens avec la valeur true?
Bref, il y a plein de raisons qui font que memset() est une fonction dangereuse - surtout en C++.
La solution
Pour corriger le problème, il ne s'agit pas d'utiliser une fonction ZeroMemory() ou autre en lieu et place de memset().
En C++, vous avez plusieurs possibilités pour initialiser une structure ou un tableau.
- s'il s'agit d'une structure POD ou d'un tableau composés de cellules de type POD (c'est à dire, si vous créer un type POD aggrégé), vous pouvez initialiser la variable avec le code suivant:
// will fail to compile if my_type is not a POD type
// otherwise, init all members of my_type to 0
my_type m_var = { 0 };
Ce code va initialiser l'ensemble des données avec la valeur 0 - avec un memset() ou équivalent - mais plus important, le compilateur vous interdira d'écrire ça si le type agrégé n'est pas un type POD.
- si le type agrégé que vous manipulez n'est pas un type POD, deux cas:
- s'il s'agit d'une variable, vous utiliser le constructeur par copie ou le constructeur par défaut. Ou un autre constructeur, ce n'est pas mon problème.
- s'il s'agit d'un tableau, vous utiliser
std::fill(), de la manière suivante:
// we create an array of cell_count cells my_type my_array[cell_count];
// and we explicitly fill cell_count cells with a copy of // a default-constructed object. std::fill(my_array, my_array + cell_count, my_type());
La façon dont la copie fonctionne est aussi relativement importante: std::fill() va automatiquement utiliser l'opérateur operator= de my_type si celui-ci a été défini. Conclusion: l'intégrité de l'objet n'est pas remise en cause, et la copie se passe au mieux[3].
Voilà, vous avez corrigé le problème. je vous ait évité des heures de recherches sur un bugs impossible à comprendre parce que "bon sang, comment une instance peut être valide et avoir son pointeur vers la vtable mis à NULL?".
Ca fera 45€, mon bon monsieur. Vous voulez une ordonnance ?
Notes
[1] le standard C++ ne précise pas la taille en bit d'un byte (avec la relation sizeof(char) == 1 byte) ou d'un quelconque type intégral. Pour ce qui est du standard, un char pourrait tout à faire faire 9 bits.
[2] Plain Old Datatype; un type POD n'a pas de constructeur ni de fonction membre, et tous ses champs sont aussi des champs de type POD.
[3] et de toute façon, vous devriez utiliser un std::vector<> pour stocker votre tableau.
Commentaires
1. Le lundi 9 juillet 2007 à 18:40, par Emmanuel Deloget
2. Le dimanche 18 mai 2008 à 20:24, par compositeur
3. Le lundi 19 mai 2008 à 15:29, par Emmanuel Deloget
4. Le vendredi 23 mai 2008 à 11:33, par r0d
5. Le vendredi 23 mai 2008 à 12:01, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire