Est-il légal d'indexer dans une structure?

104

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, bet cque (&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.

jcoder
la source
51
Non, ce n'est pas «légal». C'est un comportement indéfini.
Sam Varshavchik
10
Cela fonctionne pour vous dans ce cas très simple car le compilateur n'ajoute aucun remplissage entre les membres. Essayez avec des structures utilisant des types de tailles différentes et elles s'effondreront.
Un mec programmeur
7
Creuser le passé - UB était autrefois des démons nasaux surnommés .
Adrian Colomitchi
21
Eh bien super, ici je trébuche parce que je suis la balise C, lis la question, puis écris une réponse qui ne s'applique qu'à C, car je n'ai pas vu la balise C ++. C et C ++ sont très différents ici! C autorise la punition de type avec des unions, C ++ ne le fait pas.
Lundin
7
Si vous devez accéder aux éléments sous forme de tableau, définissez-les sous forme de tableau. S'ils doivent avoir des noms différents, utilisez les noms. Essayer d'avoir votre gâteau et de le manger entraînera éventuellement une indigestion - probablement au moment le plus gênant imaginable. (Je pense que l'index 0 est légal en C; l'indice 1 ou 2 ne l'est pas. Il existe des contextes dans lesquels un seul élément est traité comme un tableau de taille 1.)
Jonathan Leffler

Réponses:

73

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):

[dcl.array / 1] : ... Un objet de type tableau contient unensemble non vide alloué de manière contiguë de N sous-objets de type T ...

Mais, pour les membres, il n'y a pas d' exigence contiguë :

[class.mem / 17] : ...; Les exigences d'alignement d'implémentation peuvent empêcher deux membres adjacents d'être alloués immédiatement l'un après l'autre ...

Alors que les deux guillemets ci-dessus devraient être suffisants pour indiquer pourquoi l'indexation dans un structcomme 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:

[expr.post//expr.sub/1] : Une expression suffixe suivie d'une expression entre crochets est une expression suffixe. L'une des expressions doit être une valeur gl de type «tableau de T» ou une valeur pr de type «pointeur vers T» et l'autre doit être une valeur pr d'énumération non cadrée ou de type intégral. Le résultat est de type «T». Le type «T» doit être un type d'objet complètement défini.66 L'expression E1[E2]est identique (par définition) à((E1)+(E2))

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

[expr.add / 4] : Lorsqu'une expression de type intégral est ajoutée ou soustraite d'un pointeur, le résultat a le type de l'opérande du pointeur. Si l'expressionPpointe vers l'élémentx[i]d' un objet xtableau avec n éléments, les expressionsP + JetJ + P(oùJa la valeurj) pointent vers l'élément (éventuellement hypothétique)x[i + j] if0 ≤ i + j ≤ n; sinon , le comportement n'est pas défini. ...

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.amembre de la structure via ce partenaire.

WhiZTiM
la source
1
@jcoder Il est défini dans class.mem . Voir le dernier paragraphe pour le texte réel.
NathanOliver
4
La stricte alisation n'est pas pertinente ici. Le type int est contenu dans le type d'agrégat et ce type peut avoir un alias int. - 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),
2501
1
@Les downvoters, voulez-vous commenter? - et pour améliorer ou indiquer où cette réponse est erronée?
WhiZTiM
4
Un aliasing strict n'est pas pertinent à cet égard. Le remplissage ne fait pas partie de la valeur stockée d'un objet. Cette réponse ne répond pas non plus au cas le plus courant: que se passe-t-il lorsqu'il n'y a pas de remplissage? Je recommanderais de supprimer cette réponse en fait.
MM
1
Terminé! J'ai supprimé le paragraphe sur l'aliasing strict.
WhiZTiM
48

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:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

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.

2501
la source
3
Notez que cela ne fonctionne que si la classe est un type de disposition standard. Sinon, c'est toujours UB.
NathanOliver
@NathanOliver Je dois mentionner que ma réponse ne s'applique qu'à C. Edited. C'est l'un des problèmes de ces questions de langage à double balise.
2501
Merci, et c'est pourquoi j'ai demandé séparément C ++ et C car il est intéressant de connaître les différences
jcoder
@NathanOliver L'adresse du premier membre est garantie de coïncider avec l'adresse de la classe C ++ s'il s'agit d'une disposition standard. Cependant, cela ne garantit pas que l'accès est bien défini ni n'implique que de tels accès sur d'autres classes ne sont pas définis.
Potatoswatter
diriez-vous que cela char* p = ( char* )&thing.a + offsetof( thing , b );conduit à un comportement indéfini?
MM
43

En C ++ si vous en avez vraiment besoin - créez un opérateur []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

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:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Cette solution modifierait la taille de la structure afin que vous puissiez également utiliser des méthodes:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};
Slava
la source
1
J'adorerais voir le désassemblage de ceci, par rapport au désassemblage d'un programme C en utilisant la punition de type. Mais, mais ... C ++ est aussi rapide que C ... non? Droite?
Lundin
6
@Lundin si vous vous souciez de la vitesse de cette construction, les données doivent d'abord être organisées comme un tableau, et non comme des champs séparés.
Slava
2
@Lundin dans les deux, vous voulez dire un comportement illisible et indéfini? Non merci.
Slava
1
La surcharge de l'opérateur @Lundin est une fonctionnalité syntaxique à la compilation qui n'induit aucune surcharge par rapport aux fonctions normales. Jetez un œil à godbolt.org/g/vqhREz pour voir ce que fait réellement le compilateur lorsqu'il compile le code C ++ et C. C'est incroyable ce qu'ils font et ce que l'on attend d'eux. Personnellement, je préfère une meilleure sécurité de type et une meilleure expressivité de C ++ sur C un million de fois. Et cela fonctionne tout le temps sans compter sur des hypothèses concernant le rembourrage.
Jens
2
Ces références doubleront au moins la taille de la chose. Fais juste thing.a().
TC
14

Pour c ++: si vous devez accéder à un membre sans connaître son nom, vous pouvez utiliser un pointeur vers une variable membre.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;
Conteur - Unslander Monica
la source
1
Ceci utilise les facilités linguistiques, et par conséquent est bien défini et, comme je suppose, efficace. Meilleure réponse.
Peter - Réintègre Monica
2
Supposez-vous efficace? Je suppose le contraire. Regardez le code généré.
JDługosz
1
@ JDługosz, vous avez tout à fait raison. En jetant un coup d'œil à l'assembly généré, il semble que gcc 6.2 crée du code équivalent à l'utilisation offsetoffen C.
StoryTeller - Unslander Monica
3
vous pouvez également améliorer les choses en créant arr constexpr. Cela créera une seule table de recherche fixe dans la section de données plutôt que de la créer à la volée.
Tim
10

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 soi switch()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.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

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.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

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

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

C'est évidemment terrible, comparé à la version de punition de type union basée sur C (ou GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]
Peter Cordes
la source
@MM: bon point. C'est plus une réponse à divers commentaires et une alternative à la réponse de Slava. J'ai reformulé le début, donc cela commence au moins comme une réponse à la question initiale. Merci d'avoir fait remarquer cela.
Peter Cordes
Alors que le poinçonnage de type basé sur l'union semble fonctionner dans gcc et clang tout en utilisant l' []opérateur directement sur un membre d'union, le Standard définit array[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 que someUnion.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.
supercat
9

En C ++, il s'agit principalement d' un comportement non défini (cela dépend de l'index).

De [expr.unary.op]:

Aux fins de l'arithmétique des pointeurs (5.7) et de la comparaison (5.9, 5.10), un objet qui n'est pas un élément de tableau dont l'adresse est prise de cette manière est considéré comme appartenant à un tableau avec un élément de type T.

L'expression &thing.aest donc considérée comme faisant référence à un tableau de un int.

De [expr.sub]:

L'expression E1[E2]est identique (par définition) à*((E1)+(E2))

Et de [expr.add]:

Lorsqu'une expression de type intégral est ajoutée ou soustraite à un pointeur, le résultat a le type de l'opérande du pointeur. Si l'expression Ppointe vers l'élément x[i]d'un objet tableau xavec des néléments, les expressions P + Jet J + P(où Ja la valeur j) pointent vers l'élément (éventuellement hypothétique) x[i + j]if 0 <= i + j <= n; sinon, le comportement n'est pas défini.

(&thing.a)[0]est parfaitement bien formé car il &thing.aest 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 que 0 <= i + j <= n, puisque nous avons i == 0, j == 2, n == 1. Construire simplement le pointeur &thing.a + 2est 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]:

Une valeur d'un type pointeur qui est un pointeur vers ou après la fin d'un objet représente l'adresse du premier octet en mémoire (1.7) occupé par l'objet53 ou le premier octet en mémoire après la fin du stockage occupé par l'objet , respectivement. [Remarque: un pointeur au-delà de la fin d'un objet (5.7) n'est pas considéré comme pointant vers un objet non lié du type d'objet qui pourrait être situé à cette adresse.

Par conséquent, prendre le pointeur &thing.a + 1est un comportement défini, mais le déréférencer n'est pas défini car il ne pointe vers rien.

Barry
la source
Evaluer (& thing.a) + 1 est à peu près légal car un pointeur au-delà de la fin d'un tableau est légal; lire ou écrire les données stockées là-bas est un comportement indéfini, comparer avec & thing.b avec <,>, <=,> = est un comportement non défini. (& thing.a) + 2 est absolument illégal.
gnasher729
@ gnasher729 Oui, cela vaut la peine de clarifier davantage la réponse.
Barry
C'est (&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 ++?
WhiZTiM
C'est aussi un cas très important car sinon, toute boucle utilisant des pointeurs comme intervalle semi-ouvert serait UB.
Jens
Concernant la dernière citation standard. C ++ doit être mieux spécifié que C ici.
2501
8

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 écritures thing.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.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

ici, vous obtenez généralement un compilateur disant 7 puis 2! = 7, puis deux pointeurs identiques; malgré le fait qui ptrpointe vers x. Le compilateur prend le fait qu'il xs'agit d'une valeur constante pour ne pas prendre la peine de la lire lorsque vous demandez la valeur de x.

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ù il xa été modifié, le compilateur est libre de ne pas le lire réellement lors de la lecture x!

Le compilateur peut devenir assez intelligent pour comprendre comment éviter même de suivre ptrpour lire *ptr, mais ce n'est souvent pas le cas. N'hésitez pas à utiliser ptr = ptr+argc-1ou à utiliser une telle confusion si l'optimiseur devient plus intelligent que vous.

Vous pouvez fournir une personnalisation operator[]qui obtient le bon article.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

avoir les deux est utile.

Yakk - Adam Nevraumont
la source
"le fait qu'il ne soit pas membre d'un tableau signifie que le compilateur peut supposer qu'aucun accès au tableau basé sur [] ne peut le modifier." - faux, par exemple (&thing.a)[0]peut le modifier
MM
Je ne vois pas en quoi l'exemple de const a quelque chose à voir avec la question. Cela échoue uniquement parce qu'il existe une règle spécifique selon laquelle un objet const ne peut pas être modifié, pas pour une autre raison.
MM
1
@MM, ce n'est pas un exemple d'indexation dans une structure, mais c'est une très bonne illustration de la façon dont l'utilisation d'un comportement non défini pour référencer quelque chose par son emplacement apparent en mémoire, peut entraîner une sortie différente de celle attendue, car le compilateur peut faire autre chose avec l'UB que vous le vouliez.
Wildcard
@MM Désolé, pas d'accès à un tableau autre qu'un trivial via un pointeur vers l'objet lui-même. Et le second n'est qu'un exemple des effets secondaires faciles à voir d'un comportement indéfini; le compilateur optimise les lectures xcar il sait que vous ne pouvez pas le modifier d'une manière définie. Une optimisation similaire peut se produire lorsque vous modifiez bvia (&blah.a)[1]si le compilateur peut prouver qu'il n'y avait pas d'accès défini à ce bqui 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.
Yakk - Adam Nevraumont
6

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 Datastructure 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 son arélément de réseau, ainsi que beginet les endfonctions, 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;, un Proxyobjet est renvoyé. Puis, comme cette Proxyrvalue 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' Proxyobjet est instancié et reste valide pendant l'évaluation de l'expression initiale.

La construction d'un Proxyobjet remplit ses 3 membres de référence a, bet cselon 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èle T. Ainsi, au lieu d'utiliser des références nommées qui sont membres de la Dataclasse, 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 de main(). La #if 1version utilise les opérateurs ->et [], et la #if 0version exécute l'ensemble de procédures équivalent, mais uniquement en accédant directement Data::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 chaque std::cout <<appel.

Pour gcc 6.2, en utilisant -O3, les deux versions de main()génèrent le même assembly (basculez entre #if 1et #if 0avant le premier main()à comparer): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif
Christopher Oicles
la source
Nifty. Vote positif principalement parce que vous avez prouvé que cela optimise loin. BTW, vous pouvez le faire beaucoup plus facilement en écrivant une fonction très simple, pas un tout main()avec des fonctions de synchronisation! par exemple, int getb(Data *d) { return (*d)->b; }compile uniquement mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Oui, Data &dcela 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.)
Peter Cordes
Bref, c'est cool. D'autres idées comme int tmp[] = { a, b, c}; return tmp[idx];ne pas optimiser, c'est donc bien que celle-ci le fasse.
Peter Cordes
Une raison de plus qui me manque operator.dans C ++ 17.
Jens
2

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é:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Pour la version C ++ uniquement, vous voudrez probablement l'utiliser static_assertpour vérifier que la disposition struct dataest standard, et peut-être lancer une exception sur un index non valide à la place.

Hyde
la source
1

C'est illégal, mais il existe une solution de contournement:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Vous pouvez maintenant indexer v:

Sven Nilsson
la source
6
De nombreux projets C ++ pensent que le downcasting partout est très bien. Nous ne devons toujours pas prêcher de mauvaises pratiques.
StoryTeller - Unslander Monica
2
L'union résout le problème d'aliasing strict dans les deux langues. Mais le poinçonnage de type via des unions ne convient qu'en C, pas en C ++.
Lundin
1
encore, je ne serais pas surpris si cela fonctionne sur 100% de tous les compilateurs c ++. déjà.
Sven Nilsson
1
Vous pouvez l'essayer dans gcc avec les paramètres d'optimisation les plus agressifs.
Lundin
1
@Lundin: la punition de type union est légale dans GNU C ++, en tant qu'extension sur ISO C ++. Cela ne semble pas être indiqué très clairement dans le manuel , mais j'en suis presque sûr. Pourtant, cette réponse doit expliquer où elle est valide et où elle ne l'est pas.
Peter Cordes