web-dev-qa-db-fra.com

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

Indépendamment de la "mauvaise" qualité du code, et en supposant que l'alignement, etc., ne pose pas de problème sur le compilateur/la plate-forme, s'agit-il d'un comportement 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éder a, b et c comme (&thing.a)[0], (&thing.a)[1], et (&thing.a)[2]?

Dans tous les cas, sur chaque compilateur et plate-forme sur lesquels je l'ai essayé, chaque réglage que j'ai essayé a fonctionné. Je crains juste que le compilateur ne réalise pas que b et chose [1] sont la même chose et que les magasins vers 'b' pourraient être mis dans un registre [1] lit la mauvaise valeur dans la mémoire (par exemple). Dans tous les cas, j'ai essayé de faire ce qui était juste. (Je réalise bien sûr que cela ne prouve pas grand chose)

Ce n'est pas mon code c'est le code avec lequel je dois travailler, je m'intéresse à savoir si c'est mauvais code ou cassé code, car cela affecte mes priorités pour le changer beaucoup :)

Tagged C et C++. Je m'intéresse surtout au C++ mais aussi au C si c'est différent, juste pour l'intérêt.

102
jcoder

C'est illégal 1. C'est un comportement indéfini en C++.

Vous prenez les membres comme un tableau, mais voici ce que dit la norme C++ (c'est moi qui souligne):

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

Mais, pour les membres, il n’existe pas une telle exigence contiguë:

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

Les deux guillemets ci-dessus devraient suffire à nous faire comprendre pourquoi l'indexation dans un struct comme vous l'avez fait n'est pas un comportement défini par la norme C++, prenons un exemple: examinons l'expression (&thing.a)[2] - Concernant l’opérateur indice:

[expr.post//expr.sub/1] : Une expression postfixée suivie d'une expression entre crochets est une expression postfixée. L'une des expressions doit être une valeur glvalue de type "tableau de T" ou une valeur prype de type "pointeur sur T" et l'autre doit être une valeur de type énumération non rognée ou 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: à propos de l'ajout d'un type intégral à un type de pointeur (notez l'emphase ici) ..

[expr.add/4] : Lorsqu'une expression de type intégral est ajoutée ou soustraite à un pointeur, le résultat a le type de la pointeur opérande. Si l'expression P pointe sur l'élément x[i] de un tableau objet x avec n éléments, les expressions P + J et J + P (où J a la valeur j) pointe sur l'élément (éventuellement hypothétique) x[i + j] si 0 ≤ i + j ≤ n; sinon , le comportement n'est pas défini. ...

Notez l'exigence array pour la clause if ; sinon le sinon dans la citation ci-dessus. L'expression (&thing.a)[2] évidemment, ne remplit pas les conditions pour la clause if ; Par conséquent, le comportement indéfini.


Sur une note de côté: Bien que j'aie beaucoup expérimenté le code et ses variations sur différents compilateurs et qu'ils n'introduisent aucun remplissage ici, (it 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 procéder ainsi. 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 souligné à juste titre dans les commentaires, [basic.lval/8], qui était dans ma précédente édition ne s'applique pas. Merci @ 2501 et @ M.M.

1: Voir la réponse de @ Barry à cette question pour le seul cas juridique où vous pouvez accéder à thing.a membre de la structure via ce partenaire.

71
WhiZTiM

Non. En C, il s'agit d'un comportement non défini même s'il n'y a pas de remplissage.

La cause du comportement indéfini est l’accès hors limites.1. Quand vous avez un scalaire (membres a, b, c dans la structure) et essayez de l’utiliser comme un tableau2 pour accéder à l'élément hypothétique suivant, vous induisez un comportement indéfini, même s'il se trouve qu'un autre objet du même type se trouve à 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/IEC 9899: 201x 6.5.6 Opérateurs d'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/IEC 9899: 201x 6.5.6 Opérateurs d'additifs 7)
Pour les besoins de ces opérateurs, un pointeur sur un objet qui n'est pas un élément d'un tableau se comporte comme un pointeur sur le premier élément d'un tableau de longueur un avec le type de l'objet comme élément. type.

49
2501

En C++ si vous en avez vraiment besoin - créez l'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

son fonctionnement est non seulement garanti, mais son 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 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]; }
};
43
Slava

Pour c ++: Si vous devez accéder à un membre sans connaître son nom, vous pouvez utiliser un pointeur sur 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;
14
StoryTeller

Dans ISO C99/C11, la punition de type basée sur les syndicats est légale. Vous pouvez donc l'utiliser au lieu d'indexer des pointeurs sur des non-tableaux (voir diverses autres réponses).

ISO C++ n'autorise pas le typage basé sur les syndicats. GNU C++ le fait, en tant qu’extension , et je pense que d’autres compilateurs qui ne supportent pas les extensions GNU supportent en général l’union-punning. Cependant, cela ne marche pas. t vous aider à écrire du code strictement portable.

Avec les versions actuelles de gcc et de clang, écrire une fonction membre C++ en utilisant un switch(idx) pour sélectionner un membre sera optimisé pour les index constants au moment de la compilation, mais produira un terrible asm ramifié pour les index d'exécution. Il n'y a rien de mal en soi avec switch() pour cela; Il s'agit simplement d'un bug d'optimisation manquée dans les compilateurs actuels. Ils pourraient compiler Slava 'switch () fonctionner efficacement.


La solution de contournement à cela consiste à le faire de l’autre façon: donnez à votre classe/structure un membre du tableau et écrivez des fonctions d’accesseur pour associer 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 examiner la sortie asm pour différents cas d'utilisation, sur le Explorateur du compilateur Godbolt . Ce sont des fonctions complètes du System V x86-64, avec l’instruction RET fin omise pour mieux montrer ce que vous obtiendriez lorsqu’elles sont en ligne. ARM/MIPS/quoi que ce soit semblable.

# 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]

À titre de comparaison, la réponse de @ Slava utilisant switch() pour C++ est identique à celle-ci pour un index de variable d'exécution. (Code dans le lien précédent Godbolt).

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

Ceci est évidemment terrible par rapport à la version de punning de type union 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]
10
Peter Cordes

C'est un comportement indéfini.

De nombreuses règles en C++ tentent de donner au compilateur un espoir de comprendre ce que vous faites, afin qu'il puisse raisonner à son sujet et l'optimiser.

Il existe des règles sur le crénelage (accès aux données via deux types de pointeur 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 au tableau basé sur [] Ne peut le modifier. Ainsi, il n’est pas nécessaire de recharger constamment les données de la mémoire chaque fois que vous les utilisez; seulement si quelqu'un aurait pu le modifier de son nom.

Ainsi, le compilateur peut supposer que (&thing.a)[1] Ne fait pas référence à thing.b. Il peut utiliser ce fait pour réorganiser les lectures et les écritures dans thing.b, Ce qui invalide ce que vous voulez qu'il fasse sans invalider ce que vous lui avez réellement demandé de faire.

Un exemple classique de cela est de jeter 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 que ptr pointe sur x. Le compilateur prend le fait que x est une valeur constante pour ne pas la lire lorsque vous demandez la valeur de x.

Mais lorsque vous prenez l'adresse de x, vous la forcez à exister. Vous jetez ensuite const et modifiez-le. Donc, l'emplacement réel en mémoire où x est modifié, le compilateur est libre de ne pas le lire réellement lors de la lecture de x!

Le compilateur peut être 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 à utiliser ptr = ptr+argc-1 Ou à une telle confusion si l'optimiseur devient plus intelligent que vous.

Vous pouvez fournir une operator[] Personnalisée qui obtient le bon élément.

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

avoir les deux est utile.

8

En C++, il s'agit du comportement généralement indéfini (cela dépend de l'index).

De [expr.unary.op]:

Aux fins de l'arithmétique de pointeur (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.

On considère donc que l'expression &thing.a Fait 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 de pointeur. Si l'expression P pointe sur l'élément x[i] D'un objet tableau x avec les éléments n, les expressions P + J Et J + P (où J a la valeur j) pointe sur 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 formé car &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] Viole la condition préalable que 0 <= i + j <= n, Puisque nous avons i == 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. En réalité, cela ne viole rien dans [expr.add]. Nous sommes autorisés à prendre un pointeur après la fin du tableau, ce qui serait le cas. Ici, nous passons à une note dans [basic.compound]:

Une valeur de type pointeur correspondant à la fin d'un objet ou au-delà de celle-ci 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 de la mémoire occupée par l'objet. , respectivement. [Remarque: Un pointeur situé au-delà de la fin d'un objet (5.7) n'est pas considéré comme pointant vers un objet du même type que celui qui n'est pas associé et qui pourrait être situé à cette adresse.

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

8
Barry

Voici un moyen d'utiliser une classe proxy pour accéder aux éléments d'un tableau de membres par leur nom. Il est très C++ et ne présente aucun avantage par rapport aux fonctions d’accès renvoyant par renvoi, sauf pour les préférences syntaxiques. Ceci surcharge l'opérateur -> Pour accéder aux éléments en tant que membres. Pour être acceptable, il faut donc à la fois détester la syntaxe des accesseurs (d.a() = 5;) et tolérer l'utilisation de -> avec un objet non-pointeur. Je suppose que cela pourrait également semer la confusion chez les lecteurs qui ne sont pas familiers avec le code. Il s’agit donc peut-être d’une astuce plus astucieuse que de quelque chose que vous souhaitez mettre en production.

La structure Data de ce code inclut également des surcharges pour l'opérateur indice, pour accéder aux éléments indexés dans son membre du tableau ar, ainsi que begin et end. fonctions, pour itération. En outre, toutes ces versions sont surchargées de versions non constantes et constantes, qui, à mon avis, devaient être incluses pour être complètes.

Lorsque Data-> Est utilisé pour accéder à un élément par son nom (comme ceci: my_data->b = 5;), Un objet Proxy est renvoyé. Ensuite, comme cette Proxyvaleur n'est pas un pointeur, son propre opérateur -> Est appelé automatiquement par une chaîne, ce qui renvoie un pointeur sur lui-même. De cette façon, l'objet Proxy est instancié et reste valide lors de l'évaluation de l'expression initiale.

La contruction d’un objet Proxy renseigne ses 3 membres de référence a, b et c selon un pointeur passé dans le constructeur, supposé pointer sur un tampon contenant au moins 3 valeurs dont le type est donné en tant que paramètre de modèle T. Ainsi, au lieu d'utiliser des références nommées qui sont membres de la classe Data, cela économise de la mémoire en renseignant les références au point d'accès (mais, malheureusement, en utilisant -> Et non . opérateur).

Afin de tester dans quelle mesure l'optimiseur du compilateur élimine toute l'indirection introduite par l'utilisation de Proxy, le code ci-dessous inclut 2 versions de main(). La version #if 1 Utilise les opérateurs -> Et [], Et la version #if 0 Exécute l'ensemble de procédures équivalent, mais uniquement en accédant directement à Data::ar.

La fonction Nci() génère des valeurs entières au moment de l'exécution pour l'initialisation des éléments du tableau, ce qui empêche l'optimiseur de simplement insérer des valeurs constantes directement dans chaque appel std::cout<<.

Pour gcc 6.2, en utilisant -O3, les deux versions de main() génèrent le même assemblage (basculez entre #if 1 Et #if 0 Avant 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
6
Christopher Oicles

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 optimiser les choses, ou si struct ne contient 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 utiliser static_assert pour vérifier que struct data a une disposition standard et peut-être une exception sur un index non valide.

2
hyde

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:

1
Sven Nilsson