Indépendamment de la gravité du code, et en supposant que l'alignement, etc. n'est pas un problème sur le compilateur / la plate-forme, ce comportement est-il indéfini ou cassé?
Si j'ai une structure comme celle-ci: -
struct data
{
int a, b, c;
};
struct data thing;
Est - il légal d'accès a
, b
et c
que (&thing.a)[0]
, (&thing.a)[1]
et (&thing.a)[2]
?
Dans tous les cas, sur chaque compilateur et plate-forme, je l'ai essayé, avec chaque paramètre que j'ai essayé, cela `` fonctionnait ''. Je crains juste que le compilateur ne se rende pas compte que b et thing [1] sont la même chose et que les magasins dans 'b' peuvent être placés dans un registre et que thing [1] lit la mauvaise valeur de la mémoire (par exemple). Dans tous les cas, j'ai essayé, cela a fait la bonne chose. (Je réalise bien sûr que cela ne prouve pas grand-chose)
Ce n'est pas mon code; c'est du code avec lequel je dois travailler, je m'intéresse à savoir si c'est du mauvais code ou du code cassé car les différents affectent mes priorités pour le changer beaucoup :)
Tagged C et C ++. Je m'intéresse surtout au C ++ mais aussi au C s'il est différent, juste pour l'intérêt.
Réponses:
C'est illégal 1 . C'est un comportement non défini en C ++.
Vous prenez les membres sous forme de tableau, mais voici ce que dit la norme C ++ (c'est moi qui souligne):
Mais, pour les membres, il n'y a pas d' exigence contiguë :
Alors que les deux guillemets ci-dessus devraient être suffisants pour indiquer pourquoi l'indexation dans un
struct
comme vous l'avez fait n'est pas un comportement défini par le standard C ++, prenons un exemple: regardez l'expression(&thing.a)[2]
- Concernant l'opérateur d'indice:Creuser dans le texte en gras de la citation ci-dessus: concernant l'ajout d'un type intégral à un type pointeur (notez l'accent mis ici).
Notez l' exigence de tableau pour la clause if ; sinon le contraire dans la citation ci-dessus. L'expression
(&thing.a)[2]
n'est évidemment pas qualifiée pour la clause if ; Par conséquent, un comportement indéfini.Sur une note latérale: Bien que j'aie beaucoup expérimenté le code et ses variations sur divers compilateurs et qu'ils n'introduisent aucun remplissage ici, (cela fonctionne ); du point de vue de la maintenance, le code est extrêmement fragile. vous devez toujours affirmer que l'implémentation a alloué les membres de manière contiguë avant de faire cela. Et restez dans les limites :-). Mais son comportement reste indéfini ...
Certaines solutions de contournement viables (avec un comportement défini) ont été fournies par d'autres réponses.
Comme indiqué à juste titre dans les commentaires, [basic.lval / 8] , qui était dans ma précédente édition ne s'applique pas. Merci @ 2501 et @MM
1 : Voir la réponse de @ Barry à cette question pour le seul cas juridique où vous pouvez accéder au
thing.a
membre de la structure via ce partenaire.la source
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Non. En C, il s'agit d'un comportement indéfini même s'il n'y a pas de remplissage.
La chose qui cause un comportement indéfini est l'accès hors limites 1 . Lorsque vous avez un scalaire (membres a, b, c dans la structure) et que vous essayez de l'utiliser comme tableau 2 pour accéder à l'élément hypothétique suivant, vous provoquez un comportement indéfini, même s'il se trouve qu'il y a un autre objet du même type à cette adresse.
Cependant, vous pouvez utiliser l'adresse de l'objet struct et calculer le décalage dans un membre spécifique:
Cela doit être fait pour chaque membre individuellement, mais peut être placé dans une fonction qui ressemble à un accès à un tableau.
1 (Extrait de: ISO / CEI 9899: 201x 6.5.6 Opérateurs additifs 8)
Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un opérateur unaire * évalué.
2 (Extrait de: ISO / CEI 9899: 201x 6.5.6 Opérateurs additifs 7)
Pour les besoins de ces opérateurs, un pointeur vers un objet qui n'est pas un élément d'un tableau se comporte comme un pointeur vers le premier élément d'un tableau de longueur un avec le type de l'objet comme type d'élément.
la source
char* p = ( char* )&thing.a + offsetof( thing , b );
conduit à un comportement indéfini?En C ++ si vous en avez vraiment besoin - créez un opérateur []:
il est non seulement garanti de fonctionner, mais l'utilisation est plus simple, vous n'avez pas besoin d'écrire une expression illisible
(&thing.a)[0]
Remarque: cette réponse est donnée en supposant que vous avez déjà une structure avec des champs et que vous devez ajouter un accès via un index. Si la vitesse est un problème et que vous pouvez modifier la structure, cela pourrait être plus efficace:
Cette solution modifierait la taille de la structure afin que vous puissiez également utiliser des méthodes:
la source
thing.a()
.Pour c ++: si vous devez accéder à un membre sans connaître son nom, vous pouvez utiliser un pointeur vers une variable membre.
la source
offsetoff
en C.Dans l'ISO C99 / C11, le poinçonnage de type basé sur l'union est légal, vous pouvez donc l'utiliser au lieu d'indexer des pointeurs vers des non-tableaux (voir diverses autres réponses).
ISO C ++ n'autorise pas le poinçonnage de type basé sur l'union. GNU C ++ le fait, en tant qu'extension , et je pense que certains autres compilateurs qui ne supportent pas les extensions GNU en général prennent en charge le poinçonnage de type union. Mais cela ne vous aide pas à écrire du code strictement portable.
Avec les versions actuelles de gcc et clang, écrire une fonction membre C ++ en utilisant a
switch(idx)
pour sélectionner un membre optimisera les index constants au moment de la compilation, mais produira de terribles asm branchés pour les index d'exécution. Il n'y a rien de mal en soiswitch()
pour cela; il s'agit simplement d'un bogue d'optimisation manquée dans les compilateurs actuels. Ils pourraient compiler efficacement la fonction switch () de Slava.La solution / solution de contournement à cela est de le faire dans l'autre sens: donnez à votre classe / structure un membre de tableau et écrivez des fonctions d'accesseur pour attacher des noms à des éléments spécifiques.
Nous pouvons jeter un œil à la sortie asm pour différents cas d'utilisation, sur l' explorateur du compilateur Godbolt . Ce sont des fonctions System V x86-64 complètes, avec l'instruction RET de fin omise pour mieux montrer ce que vous obtiendriez lorsqu'elles sont en ligne. ARM / MIPS / tout ce qui serait similaire.
Par comparaison, la réponse de @ Slava utilisant un
switch()
pour C ++ rend asm comme ceci pour un index de variable d'exécution. (Code dans le lien Godbolt précédent).C'est évidemment terrible, comparé à la version de punition de type union basée sur C (ou GNU C ++):
la source
[]
opérateur directement sur un membre d'union, le Standard définitarray[index]
comme étant équivalent à*((array)+(index))
, et ni gcc ni clang ne reconnaîtront de manière fiable qu'un accès à*((someUnion.array)+(index))
est un accès àsomeUnion
. La seule explication que je peux voir est quesomeUnion.array[index]
ni*((someUnion.array)+(index))
ne sont définis par le Standard, mais sont simplement des extensions populaires, et gcc / clang a choisi de ne pas prendre en charge la seconde mais semble prendre en charge la première, du moins pour le moment.En C ++, il s'agit principalement d' un comportement non défini (cela dépend de l'index).
De [expr.unary.op]:
L'expression
&thing.a
est donc considérée comme faisant référence à un tableau de unint
.De [expr.sub]:
Et de [expr.add]:
(&thing.a)[0]
est parfaitement bien formé car il&thing.a
est considéré comme un tableau de taille 1 et nous prenons ce premier index. C'est un index autorisé à prendre.(&thing.a)[2]
constitue une violation de la condition que0 <= i + j <= n
, puisque nous avonsi == 0
,j == 2
,n == 1
. Construire simplement le pointeur&thing.a + 2
est un comportement indéfini.(&thing.a)[1]
est le cas intéressant. Il ne viole en fait rien dans [expr.add]. Nous sommes autorisés à prendre un pointeur au-delà de la fin du tableau - ce qui serait. Ici, nous passons à une note dans [basic.compound]:Par conséquent, prendre le pointeur
&thing.a + 1
est un comportement défini, mais le déréférencer n'est pas défini car il ne pointe vers rien.la source
(&thing.a + 1)
un cas intéressant que je n'ai pas réussi à couvrir. +1! ... Juste curieux, êtes-vous membre du comité ISO C ++?Il s'agit d'un comportement indéfini.
Il existe de nombreuses règles en C ++ qui tentent de donner au compilateur l'espoir de comprendre ce que vous faites, afin qu'il puisse raisonner et l'optimiser.
Il existe des règles concernant l'aliasing (accès aux données via deux types de pointeurs différents), les limites de tableau, etc.
Lorsque vous avez une variable
x
, le fait qu'elle ne soit pas membre d'un tableau signifie que le compilateur peut supposer qu'aucun[]
accès basé sur un tableau ne peut la modifier. Il n'est donc pas nécessaire de recharger constamment les données de la mémoire chaque fois que vous l'utilisez; seulement si quelqu'un aurait pu le modifier à partir de son nom .Ainsi,
(&thing.a)[1]
le compilateur peut supposer qu'il ne fait pas référence àthing.b
. Il peut utiliser ce fait pour réorganiser les lectures et les écrituresthing.b
, invalider ce que vous voulez qu'il fasse sans invalider ce que vous lui avez réellement dit de faire.Un exemple classique de ceci est de rejeter const.
ici, vous obtenez généralement un compilateur disant 7 puis 2! = 7, puis deux pointeurs identiques; malgré le fait qui
ptr
pointe versx
. Le compilateur prend le fait qu'ilx
s'agit d'une valeur constante pour ne pas prendre la peine de la lire lorsque vous demandez la valeur dex
.Mais lorsque vous prenez l'adresse de
x
, vous la forcez à exister. Vous rejetez ensuite const et vous le modifiez. Ainsi, l'emplacement réel en mémoire où ilx
a été modifié, le compilateur est libre de ne pas le lire réellement lors de la lecturex
!Le compilateur peut devenir assez intelligent pour comprendre comment éviter même de suivre
ptr
pour lire*ptr
, mais ce n'est souvent pas le cas. N'hésitez pas à utiliserptr = ptr+argc-1
ou à utiliser une telle confusion si l'optimiseur devient plus intelligent que vous.Vous pouvez fournir une personnalisation
operator[]
qui obtient le bon article.avoir les deux est utile.
la source
(&thing.a)[0]
peut le modifierx
car il sait que vous ne pouvez pas le modifier d'une manière définie. Une optimisation similaire peut se produire lorsque vous modifiezb
via(&blah.a)[1]
si le compilateur peut prouver qu'il n'y avait pas d'accès défini à ceb
qui pourrait le modifier; un tel changement pourrait se produire en raison de changements apparemment inoffensifs dans le compilateur, le code environnant ou autre. Donc, même tester que cela fonctionne n'est pas suffisant.Voici un moyen d'utiliser une classe proxy pour accéder aux éléments d'un tableau de membres par nom. Il est très C ++ et n'a aucun avantage par rapport aux fonctions d'accesseur renvoyant ref, sauf pour la préférence syntaxique. Cela surcharge l'
->
opérateur pour accéder aux éléments en tant que membres, donc pour être acceptable, il faut à la fois ne pas aimer la syntaxe des accesseurs (d.a() = 5;
), ainsi que tolérer l'utilisation->
avec un objet non pointeur. Je pense que cela pourrait également dérouter les lecteurs qui ne sont pas familiers avec le code, donc cela pourrait être plus une astuce intéressante que quelque chose que vous souhaitez mettre en production.La
Data
structure de ce code inclut également les surcharges pour l'opérateur d'indexation, à des éléments indexés d'accès à l' intérieur de sonar
élément de réseau, ainsi quebegin
et lesend
fonctions, par itération. En outre, tous ces éléments sont surchargés de versions non const et const, qui, à mon avis, devaient être incluses par souci d'exhaustivité.Lorsque
Data
's->
est utilisé pour accéder à un élément par son nom (comme ceci:)my_data->b = 5;
, unProxy
objet est renvoyé. Puis, comme cetteProxy
rvalue n'est pas un pointeur, son propre->
opérateur est appelé automatiquement en chaîne, qui renvoie un pointeur sur lui-même. De cette façon, l'Proxy
objet est instancié et reste valide pendant l'évaluation de l'expression initiale.La construction d'un
Proxy
objet remplit ses 3 membres de référencea
,b
etc
selon un pointeur passé dans le constructeur, qui est supposé pointer vers un tampon contenant au moins 3 valeurs dont le type est donné comme paramètre de modèleT
. Ainsi, au lieu d'utiliser des références nommées qui sont membres de laData
classe, cela économise de la mémoire en remplissant les références au point d'accès (mais malheureusement, en utilisant->
et non l'.
opérateur).Afin de tester dans quelle mesure l'optimiseur du compilateur élimine toutes les indirection introduites par l'utilisation de
Proxy
, le code ci-dessous comprend 2 versions demain()
. La#if 1
version utilise les opérateurs->
et[]
, et la#if 0
version exécute l'ensemble de procédures équivalent, mais uniquement en accédant directementData::ar
.La
Nci()
fonction génère des valeurs entières d'exécution pour l'initialisation des éléments du tableau, ce qui empêche l'optimiseur de simplement brancher des valeurs constantes directement dans chaquestd::cout
<<
appel.Pour gcc 6.2, en utilisant -O3, les deux versions de
main()
génèrent le même assembly (basculez entre#if 1
et#if 0
avant le premiermain()
à comparer): https://godbolt.org/g/QqRWZbla source
main()
avec des fonctions de synchronisation! par exemple,int getb(Data *d) { return (*d)->b; }
compile uniquementmov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Oui,Data &d
cela rendrait la syntaxe plus facile, mais j'ai utilisé un pointeur au lieu de ref pour souligner l'étrangeté de la surcharge de->
cette façon.)int tmp[] = { a, b, c}; return tmp[idx];
ne pas optimiser, c'est donc bien que celle-ci le fasse.operator.
dans C ++ 17.Si la lecture des valeurs est suffisante et que l'efficacité n'est pas un problème, ou si vous faites confiance à votre compilateur pour bien optimiser les choses, ou si struct ne fait que 3 octets, vous pouvez le faire en toute sécurité:
Pour la version C ++ uniquement, vous voudrez probablement l'utiliser
static_assert
pour vérifier que la dispositionstruct data
est standard, et peut-être lancer une exception sur un index non valide à la place.la source
C'est illégal, mais il existe une solution de contournement:
Vous pouvez maintenant indexer v:
la source