Struct hack équivalent en C ++

Le struct hack où vous avez un tableau de longueur 0 en tant que dernier membre d’une structure de C90 et C99 est bien connu, et avec l’introduction de membres de tableau flexibles dans C99, nous avons même eu une manière standardisée de l’utiliser avec [] . Malheureusement, C ++ ne fournit aucune construction de ce type, et (du moins avec Clang 3.4 ), comstackr une structure avec [0] ou [] produira un avertissement de compilation avec --std=c++11 -pedantic :

 $ cat test.cpp struct hack { char filler; int things[0]; }; $ clang++ --std=c++11 -pedantic test.cpp \test.cpp:3:14: warning: zero size arrays are an extension [-Wzero-length-array] int things[0]; 

et pareillement

 $ cat test.cpp struct fam { char filler; int things[]; }; $ clang++ --std=c++11 -pedantic test.cpp \test.cpp:3:7: warning: flexible array members are a C99 feature [-Wc99-extensions] int things[]; 

Ma question est alors la suivante: disons que je veux avoir une structure contenant un tableau de taille variable comme dernier élément en C ++. Quelle est la bonne chose à faire si un compilateur prend en charge les deux? Devrais-je aller avec la struct hack [0] (qui est une extension du compilateur), ou le FAM [] (qui est une fonctionnalité C99)? Autant que je sache, cela fonctionnera soit, mais j’essaie de déterminer quel est le moindre mal.

Aussi, avant que les gens ne suggèrent de garder un int* sur un morceau de mémoire alloué séparément dans la structure, ce n’est pas une réponse satisfaisante. Je veux allouer un seul morceau de mémoire pour contenir à la fois mes éléments struct et array. L’utilisation d’un std :: vector tombe également dans la même catégorie. Si vous vous demandez pourquoi je ne veux pas utiliser un pointeur à la place, la réponse de R. à une autre question donne un bon aperçu.

Il y a eu des questions similaires ailleurs, mais aucune n’a répondu à cette question particulière:

  • Les membres de groupe flexibles sont-ils valides en C ++? : Très similaire, mais la question est de savoir si FAM est valide en C ++ (non). Je cherche une bonne raison de choisir l’un ou l’autre.
  • Variante conforme de l’ancien «struct hack» : propose une alternative, mais elle n’est ni jolie, ni toujours correcte (et si le padding était ajouté à la struct?). Accéder aux éléments plus tard n’est pas aussi e.things[42] que de faire e.things[42] .

Vous pouvez obtenir plus ou moins le même effet en utilisant une fonction membre et un reinterpret_cast :

 int* buffer() { return reinterpret_cast(this + 1); } 

Cela a un défaut majeur: cela ne garantit pas un alignement correct. Par exemple, quelque chose comme:

 struct Hack { char size; int* buffer() { return reinterpret_cast(this + 1); } }; 

est susceptible de renvoyer un pointeur mal aligné. Vous pouvez contourner ce problème en plaçant les données de la structure dans une union avec le type dont le pointeur est renvoyé. Si vous avez C ++ 11, vous pouvez déclarer:

 struct alignas(alignof(int)) Hack { char size; int* buffer() { return reinterpret_cast(this + 1); } }; 

(Je pense. Je n’ai jamais vraiment essayé cela, et je pourrais avoir quelques détails sur la syntaxe erronée.)

Cet idiome a un deuxième défaut important: il ne fait rien pour garantir que le champ de taille correspond à la taille réelle du tampon, et pire encore, il n’existe aucun moyen réel d’utiliser new ici. Pour corriger cela, vous pouvez définir un operator new et un operator delete spécifique à une classe:

 struct alignas(alignof(int)) Hack { void* operator new( size_t, size_t n ); void operator delete( void* ); Hack( size_t n ); char size; int* buffer() { return reinterpret_cast(this + 1); } }; 

Le code client devra alors utiliser l’emplacement new pour allouer:

 Hack* hack = new (20) Hack(20); 

Le client doit toujours répéter la taille, mais il ne peut pas l’ignorer.

Il existe également des techniques permettant d’empêcher la création d’instances non allouées dynamicment, etc., pour aboutir à quelque chose comme:

 struct alignas(alignof(int)) Hack { private: void operator delete( void* p ) { ::operator delete( p ); } // ban all but dynamic lifetime (and also inheritance, member, etc.) ~Hack() = default; // ban arrays void* operator new[]( size_t ) = delete; void operator delete[]( void* p ) = delete; public: Hack( size_t n ); void* operator new( size_t, size_t n ) { return ::operator new( sizeof(Hack) + n * sizeof(int) ); } char size; // Since dtor is private, we need this. void deleteMe() { delete this; } int* buffer() { return reinterpret_cast(this + 1); } }; 

Étant donné les dangers fondamentaux d’une telle classe, il est discutable de prendre autant de mesures de protection. Même avec eux, il n’est vraiment utilisable que par une personne qui comprend parfaitement toutes les contraintes et qui fait très attention. Dans tous les cas sauf les cas extrêmes, dans le code de très bas niveau, vous feriez simplement que le tampon soit std::vector et que vous en ayez fini. Dans tous les codes sauf le niveau le plus bas, la différence de performance ne valait pas le risque et les efforts.

MODIFIER:

A titre d’exemple, l’implémentation de std::basic_ssortingng g ++ utilise quelque chose de très similaire à ce qui précède, avec une struct contenant un décompte de références, la taille actuelle et la capacité actuelle (trois size_t ), suivies directement du tampon de caractères. Et comme il a été écrit bien avant C ++ 11 et alignas / alignof , quelque chose comme std::basic_ssortingng plantera sur certains systèmes (par exemple, un Sparc). (Bien qu’un bogue technique, la plupart des gens ne considèrent pas cela comme un problème critique.)

C’est du C ++, donc des modèles sont disponibles:

 template  struct hack { int filler; int thing [N]; }; 

Faire couler différents pointeurs vers différentes instanciations sera alors le problème difficile à résoudre.

La première chose qui me vient à l’esprit est NE PAS , n’écrivez pas C en C ++. Dans 99,99% des cas, ce hack n’est pas nécessaire, n’apportera aucune amélioration notable des performances par rapport au simple maintien d’un std::vector et compliquera votre vie et celle des autres responsables du projet dans lequel vous la déployez.

Si vous souhaitez une approche conforme aux normes, fournissez un type d’encapsuleur allouant de manière dynamic un bloc de mémoire suffisamment volumineux pour contenir le hack (moins le tableau) plus N*sizeof(int) pour l’équivalent du tableau (n’oubliez pas de vous assurer bon alighnment). La classe aurait des accesseurs qui mappent les membres et les éléments du tableau à l’emplacement correct en mémoire.

Ignorer l’alignement et le code de plaque de la chaudière pour rendre l’interface agréable et la mise en œuvre sûre:

 template  class DataWithDynamicArray { void *ptr; int* array() { return static_cast(static_cast(ptr)+sizeof(T)); // align! } public: DataWithDynamicArray(int size) : ptr() { ptr = malloc(sizeof(T) + sizeof(int)*size); // force correct alignment new (ptr) T(); } ~DataWithDynamicArray() { static_cast(ptr)->~T(); free(ptr); } // copy, assignment... int& operator[](int pos) { return array()[pos]; } T& data() { return *static_cast(ptr); } }; struct JustSize { int size; }; DataWithDynamicArray x(10); x.data().size = 10 for (int i = 0; i < 10; ++i) { x[i] = i; } 

Maintenant, je ne l'implémenterais vraiment pas de cette façon ( DataWithDynamicArray de l' DataWithDynamicArray !!), comme par exemple la taille devrait faire partie de l'état de DataWithDynamicArray ...

Cette réponse est fournie uniquement à titre d’exercice, pour expliquer que la même chose peut être faite sans extensions, mais méfiez-vous, ceci est un exemple de jouet qui présente de nombreux problèmes, y compris mais sans se limiter à la sécurité des exceptions ou à l’alignement (tout en étant mieux que de forcer l'utilisateur de faire le malloc avec la taille correcte). Le fait que vous puissiez le faire ne veut pas dire que vous devriez , et la vraie question est de savoir si vous avez besoin de cette fonctionnalité et si ce que vous essayez de faire est un bon design ou pas.

Si vous ressentez vraiment le besoin d’utiliser un hack, pourquoi ne pas simplement utiliser

 struct hack { char filler; int things[1]; }; 

suivi par

 hack_p = malloc(sizeof(struct hack)+(N-1)*sizeof int)); 

Ou ne vous occupez même pas du -1 et vivez avec un peu d’espace supplémentaire.

C ++ n’a pas le concept de “tableaux flexibles”. La seule façon d’avoir un tableau flexible en C ++ est d’utiliser un tableau dynamic, ce qui vous conduit à utiliser int* things . Vous aurez besoin d’un paramètre de taille si vous essayez de lire ces données à partir d’un fichier afin de pouvoir créer le tableau de taille appropriée (ou d’utiliser un std::vector et continuez à lire jusqu’à la fin du stream).

Le hack “tableau flexible” conserve la localité spatiale (c’est-à-dire que la mémoire allouée est contenue dans un bloc contigu au rest de la structure), ce que vous perdez lorsque vous êtes obligé d’utiliser la mémoire dynamic. Il n’ya pas vraiment de solution élégante à ce problème (par exemple, vous pouvez allouer un tampon important, mais vous devez le rendre suffisamment grand pour contenir le nombre d’éléments de votre choix – et si les données réellement lues sont plus petites que le tampon, il y aurait un espace gaspillé alloué).

Aussi, avant que les gens ne suggèrent de garder un int * sur un morceau de mémoire alloué séparément dans la structure, ce n’est pas une réponse satisfaisante. Je veux allouer un seul morceau de mémoire pour contenir à la fois mes éléments struct et array. L’utilisation d’un std :: vector tombe également dans la même catégorie.

C’est ce que vous feriez en C ++. Vous pouvez voter contre tout ce que vous voulez, mais le fait demeure: une extension non standard ne fonctionnera pas si vous passez à un compilateur qui ne le prend pas en charge. Si vous respectez la norme (par exemple, évitez d’utiliser des hacks spécifiques au compilateur), vous aurez moins de chances de rencontrer ce type de problème.

Il existe au moins un avantage pour les éléments de tableau flexibles par rapport aux tableaux de longueur zéro lorsque le compilateur est en mode Clang.

 struct Strukt1 { int fam[]; int size; }; struct Strukt2 { int fam[0]; int size; }; 

Ici, clang va se tromper s’il voit Strukt1 mais pas s’il voit Strukt2 . gcc et icc acceptent les erreurs sans erreur et les erreurs msvc dans les deux cas. gcc fait une erreur si le code est compilé en C.

La même chose s’applique à cet exemple similaire mais moins évident:

 struct Strukt3 { int size; int fam[]; }; strukt Strukt4 { Strukt3 s3; int i; };