09 juil. 2007

C++ et memset()

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.

  1. 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.

  1. si le type agrégé que vous manipulez n'est pas un type POD, deux cas:
    1. 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.
    2. 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, juillet 9 2007, 18:40 par Emmanuel Deloget

PS: excusez les problèmes de formattage. L'édition en mode DotClear 1.2 est certes sympathique, mais bien souvent un peut trop limitée...

2. Le dimanche, mai 18 2008, 20:24 par compositeur

Hello, cette précision est pas forcément utile ;) : "c'est a dire, si vous creer un type pod aggrege" ... en tout cas, très bon billet ! @+

3. Le lundi, mai 19 2008, 15:29 par Emmanuel Deloget

La précision est utile, car elle permet de spécifier la désignation formelle de ce type particulier d'objet telle qu'on la retrouve dans la norme C++.

4. Le vendredi, mai 23 2008, 11:33 par r0d

Très bon billet. Je me suis toujours demandé l'intérêt de cette fonction dans la SL. Peut-être est-ce juste pour faire plaisir aux gens qui viennent du C.

Je profite de ce commentaire pour faire un chtite remarque. "Si il ..." n'est pas correct en français, il faut écrire "S'il ...". ;)

5. Le vendredi, mai 23 2008, 12:01 par Emmanuel Deloget

Merci :)

Outre le fait de vouloir rassurer les gens qui viennent d'un background C, il faut bien aussi rester compatible avec le code déjà écrit. memset() reste encore très utilisé dans les "programmes qui datent un peu" :)

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet