3
votes

Types efficaces d'objets et de structures alloués

D'après les spécifications de c99, je ne comprends pas très bien ce qui se passe avec le type effectif de l'objet alloué ci-dessous.

typedef struct {
    int x;
    char y;
} MyStruct ;

MyStruct *make_struct (void) {
    MyStruct *p = malloc(sizeof(MyStruct));
    p->x = 1;
    p->y = 2;

    /* what is the effective type of the allocated object at this point? */

    return p;
}

Lorsque vous attribuez une valeur à un objet alloué, le type effectif de l'objet objet alloué devient le type de la lvalue utilisée pour le magasin, mais quelle est la lvalue utilisée ici?

D'après ce que je comprends du 6.5.2.3p4 ...

Une expression suffixe suivie de l'opérateur -> et d'un identifiant désigne un membre d'une structure ou d'un objet union. La valeur est celle du membre nommé de l'objet vers lequel pointe la première expression et est une lvalue. Si la première expression est un pointeur vers un type qualifié, le résultat a la version ainsi qualifiée du type du membre désigné.

... le type d'une expression "x-> y" est le type de y (mais seulement si x pointe vers un type qualifié).
Alors j'ai un objet alloué sans type effectif et deux "objets internes" avec des types int et char?

Quelle confusion ...

Edit: Supposons que le type effectif de * p se termine par int. Est-ce donc un comportement indéfini? Quelqu'un finira par accéder à l'objet via une lvalue de type MyStruct. L'accès à un membre implique-t-il également l'accès au type d'agrégat? Cela continue de donner ..


8 commentaires

p est toujours un pointeur vers le type MyStruct, x est un entier et y est un char, si c'est ce que vous demandez.


Eh bien, * p a un type déclaré.


Non, ce n'est pas le cas; * p est un objet alloué et n'a pas de type déclaré.


malloc () renvoie void * . Il s'agit d'un type spécial qui peut être alloué à n'importe quel autre pointeur sans cast explicite et sans provoquer d'avertissement du compilateur.


@James: Le standard C a des règles particulières sur le type effectif d'un objet qui vont au-delà du simple type de pointeur utilisé pour y accéder. Le fait que malloc renvoie un void * n'est pas informatif à cet égard.


Quelqu'un peut-il fournir un lien vers la spécification qui a 6.5.2.3p4?


@James " 4 Une expression suffixe suivie de l'opérateur -> et d'un identifiant désigne un membre d'un objet structure ou union. La valeur est celle du membre nommé de l'objet vers lequel pointe la première expression, et est un lvalue. Si la première expression est un pointeur vers un type qualifié, le résultat a la version ainsi qualifiée du type du membre désigné. "


Supposons que le type effectif de * p se termine par int. Est-ce donc un comportement indéfini? Quelqu'un finira par accéder à l'objet via une lvalue de type MyStruct port70.net/~nsz/c/c11/n1570.html#6.5p7 semblent permettre d'accéder à un objet via une lvalue de type struct / union ayant un membre avec le type effectif de l'objet.


3 Réponses :


3
votes

Le bloc alloué n'a pas de type effectif, car (1) il n'a pas de type déclaré, et (2) il n'a pas été affecté. Les parties du bloc qui correspondent aux membres x et y ont des types effectifs, mais pas le bloc entier.

Le fait de ne pas avoir de type efficace ne constitue pas un comportement indéfini, cependant : chaque membre de MyStruct renvoyé par make_struct a reçu un type effectif approprié individuellement, de sorte que le code accédant aux membres de la struct retournée reste valide.

Votre fragment de code pourrait être modifié pour utiliser un littéral composé a> pour initialiser l'ensemble de MyStruct , plutôt que d'initialiser ses composants. Cela rendrait le type effectif du bloc alloué MyStruct:

MyStruct *make_struct () {
    MyStruct *p = malloc(sizeof(MyStruct));
    *p = (MyStruct){.x = 1, .y = 2};
    return p;
}

Remarque: Cette réponse a été considérablement modifiée après une mise à jour de la question.


0 commentaires

3
votes

Les citations proviennent de C99 6.5 / 6

Le type effectif d'un objet pour un accès à sa valeur stockée est le type déclaré de l'objet, le cas échéant.

  • malloc (sizeof (MyStruct)); À ce stade, les données renvoyées n'ont pas de type effectif.
  • MyStruct * p = malloc (sizeof (MyStruct)); Toujours pas de type efficace, p pointe juste les données sans rien stocker.
  • p-> x = 1; La règle de type efficace:

    Si une valeur est stockée dans un objet n'ayant pas de type déclaré via un lvalue ayant un type qui n'est pas un type caractère, alors le type de la lvalue devient le type effectif de l'objet pour cet accès et pour les accès suivants qui ne modifient pas la valeur stockée.

    Puisque nous avons int x; la lvaleur de l'expression p-> x = 1; est int et cela devient le type effectif de ce qui est stocké dans p->x.

  • Dans le cas de p-> y , la lvaleur utilisée pour l'accès aux objets est un type de caractère, donc la règle ci-dessus ne s'applique pas. Il n'est pas non plus copié sous forme de tableau de caractères. Nous nous retrouvons dans la dernière phrase de la règle:

    Pour tous les autres accès à un objet n'ayant pas de type déclaré, le type effectif de l'objet est simplement le type de la lvalue utilisée pour l'accès.

    Signifiant que le type effectif de p-> y devient char , puisque la lvaleur de l'expression p-> y = 2; est char.

6.5.2.3/4 n'a aucune pertinence ici, à part "... et est une lvalue".

* p n'a pas de type efficace en tant que tel, car nous n'avons jamais accédé à la zone mémoire via un type struct complet. Cependant, une expression telle que MyStruct m = * make_struct (); est toujours bien définie, car la règle stricte d'aliasing permet à une structure d'accéder aux objets, étant donné que la structure contient des membres compatibles avec les types efficaces. Dans ce cas, la structure contient des membres int et char qui sont parfaitement compatibles avec les types effectifs auxquels les données font référence avec p-> x et p-> y s'est retrouvé avec.


21 commentaires

Je pense qu'il est clair que "via une lvalue" fait référence à l'expression "p-> x", et non à 1 qui représente la valeur (a.k.a rvalue) stockée.


@ zagortenay333 J'ai peut-être mal lu cette partie en effet. Cette partie de la norme est assez horriblement écrite. Je vais éditer. Le type résultant est le même dans tous les cas.


Je pense que j'ai bien compris le droit juridique cette fois. Relecture appréciée :)


Je ne vois pas comment le type efficace pourrait finir comme char. Si le type de "p-> x" est un int, alors le type effectif reste int pour toutes les opérations qui ne modifient pas (puisque "p-> y" ne peut rien changer ici.) Mais je ne comprends pas quel est le type de "p-> x" même; ce n'est pas du tout évident pour moi.


De plus, si le type effectif final est int, alors le code ci-dessus invoque un comportement indéfini, car quelqu'un accédera à un objet de type effectif int en utilisant une lvalue qui sera MyStruct. Je veux dire, pouvez-vous même accéder à une structure ou seulement à ses membres?


@ zagortenay333 Autant que le compilateur le sache, il n'y a pas de structure à cet emplacement mémoire. Il y a un int dans un emplacement et un char dans un autre, car c'est ainsi que vous y avez accédé. La partie que vous avez citée concernant l'opérateur -> le rend clair. La première expression p "pointe" (read: fait référence) à un type de structure où le membre nommé est x. Ce n'est pas vraiment écrit avec le concept de type efficace à l'esprit, puisque le type efficace a été ajouté en C99 mais le texte en 6.5.2.3 reste depuis C90.


@ zagortenay333 Concernant l'aliasing strict, la fonction ne renvoie qu'un pointeur, le contenu n'est jamais dé-référencé via un type pointeur, donc cela ne s'applique pas. Mais même quand c'est le cas, la règle stricte d'aliasing a une exception pour les structures: "un type d'agrégation ou d'union qui inclut l'un des types susmentionnés parmi ses membres". Une structure étant un type agrégé qui comprend le "type compatible avec le type effectif de l'objet" susmentionné. En clair: accéder à l'objet int via un pointeur vers une structure contenant un membre int est correct et exempt de la règle.


@ zagortenay333 Concernant votre montage, je pense avoir enfin compris la source de votre confusion. L'ensemble des données pointées par p n'a aucun type effectif à travers tout cela. p-> x a le type effectif int et p-> y a le type effectif char , respectivement. Afin de rendre l'ensemble des données efficaces, vous devez écrire quelque chose comme * p = (MyStruct) {1,2}; ce qui n'est pas le cas ici. p aurait pu aussi être un vide * et vous auriez pu écrire quelque chose d'horrible comme * (int *) p = 1 et ((char *) p) [sizeof (int)] = 2; et cela aurait eu le même effet.


"L'ensemble des données" oui c'est ce à quoi je faisais référence! Mon hypothèse était que malloc renvoie un seul objet. Le fait que ce modèle d'accès le rende comme deux objets séparés est un peu bizarre pour moi. En regardant la définition de l'objet comme "région de stockage de données", je suppose que cela a du sens. Je pense que vos deux commentaires ici devraient probablement être la réponse ici.


@ zagortenay333 Pour un type efficace, vous devez penser à malloc comme renvoyant un bloc d'octets non identifiés, autant que vous en avez demandé. Le type du pointeur n'a aucune pertinence.


@Lundin, je n'arrive pas à comprendre la différence entre "Si une valeur est stockée dans un objet n'ayant pas de type déclaré via une lvalue ayant un type qui n'est pas un type de caractère, alors le type de la lvalue devient le type effectif de l'objet ... ", par opposition à" Pour tous les autres accès à un objet n'ayant pas de type déclaré, le type effectif de l'objet est simplement le type de la lvalue utilisée pour le accès. ". Le premier exclut les types char, mais à part cela, je pense qu'ils disent tous les deux exactement la même chose - le type effectif de l'objet assigné est celui du [...]


[...] lvaleur via laquelle la valeur est définie. Quelle est la différence?


Le premier cas mentionne spécifiquement une écriture (stockage), après quoi le type effectif de cette zone passe de aucun type effectif au type utilisé lors de l'écriture dans cette zone. Le deuxième cas concerne l'accès, y compris les lectures. Prenons par exemple int * foo = malloc (sizeof * foo); . La zone vers laquelle pointe foo n'a pas de type effectif. Ensuite, nous faisons int x = * foo; , ceci est un accès en lecture lvalue et le type effectif pour cet accès spécifique est int . foo pointe toujours vers un objet sans type effectif. ->


Mais si nous faisons * foo = 1; en écrivant un int , le type effectif de ce que foo pointe vers des changements de aucun type effectif à int à partir d'ici. Cela signifie qu'un accès ultérieur de double * bar = (double *) foo); double d = * bar; est une violation stricte d'aliasing. Ou du moins c'est ainsi que tout devrait fonctionner en théorie . Ce que font les compilateurs dans la pratique est une autre histoire, malheureusement. Parce que cette partie de la norme est ambiguë et mal rédigée.


@Lundin Alors, est-il correct de dire qu'un morceau de mémoire ne peut avoir son type effectif défini qu'une seule fois? C'est à dire. - une fois que nous avons fait * foo = 1 et défini la mémoire pointée par foo sur le type effectif int , nous ne pouvons jamais faire * ((double *) foo) = 2.5 afin de définir le type effectif sur float .


@AvivCohn Oui à peu près, mais une zone peut avoir plus d'un type efficace si vous utilisez union, etc.


@Lundin, Hmm, c'est intéressant. J'ai eu une discussion sur ce sujet dans les commentaires de cette réponse à une de mes questions ( stackoverflow.com/a/61297634/3284878). Dans ce document, la personne a confirmé ma compréhension plus récente du texte, qu'il est impossible de violer le SAR en effectuant un déréférencement d'écriture. Et qu'il n'est possible de violer le SAR qu'en effectuant une lecture-déréférence. C'est à dire. - si la zone mémoire A avait un type effectif X, et que plus tard nous réaffectons son type effectif à Y, il est maintenant légal de lire-déréférencer la zone mémoire A par lvalue de type Y. Vous n'êtes pas d'accord?


@AvivCohn: L'abstraction de type effectif a des cas de coin délicats et impraticables qui ne peuvent pas être traités correctement sans entraver l'optimisation dans certains cas plus courants. La seule façon dont la règle aurait un sens serait si les auteurs supposaient qu'il serait impossible pour un compilateur de se comporter de manière significative dans tous les cas où la norme l'exigeait sans également se comporter de manière significative dans d'autres cas, et il n'était donc pas nécessaire de accueillir explicitement tous les cas qui devraient être utiles. Leur hypothèse aurait pu être correcte, puisque ni clang ni gcc ne gèrent tous les délicats ...


... valises d'angle correctement. En tant que tel, je ne pense pas qu'il y ait beaucoup de valeur à essayer d'interpréter si la norme exigerait que les implémentations tiennent compte de divers cas de coin. La plupart des implémentations peuvent être configurées pour traiter de manière fiable certains cas d'angle dont la norme n'a clairement pas besoin, et ceux qui ne sont pas configurés pour le faire ne doivent pas être approuvés pour gérer tous les cas d'angle que la norme exige. Le fait que la norme nécessite ou non une implémentation pour gérer une construction de manière utile n'implique pas qu'une implémentation particulière le fera.


@AvivCohn Pour résumer ce qui est dit dans C17 6.5./6, il y a deux cas différents: les objets qui ont un type déclaré , comme int x; , et les objets qui n'ont aucun type déclaré , tels que des morceaux retournés par malloc, des pointeurs vers des adresses où le compilateur ne peut pas savoir ce qui est stocké etc. Le premier ne peut jamais changer leur type effectif, mais les objets sans type déclaré peuvent avoir le type effectif modifié par un accès en écriture.


@AvivCohn N'inventez pas non plus de nouvelles abréviations à trois lettres, c'est une maladie. «SAR» dans le contexte de l'ingénierie informatique signifie déjà registre d'approximation successive et se réfère à une certaine catégorie de convertisseurs analogique-numérique (SAR ADC).



0
votes

Le terme "objet" tel qu'il est utilisé partout dans le brouillon C11 (N1570) sauf 6.5p6 (la "règle de type effectif") fait référence à une région de stockage associée à un type particulier. Si int * p est un pointeur non nul valide, il pointera "vers", ou "juste après", un objet de type int . Il semble que 6.5p6 utilise le terme "objet" pour désigner une sorte de région de stockage qui peut ou non être réellement un objet, mais le reste de la norme n'utilise pas le terme "objet" de cette manière. Entre autres choses, la spécification de malloc ne dit pas qu'il renvoie un pointeur vers un objet, ni qu'il crée un objet, mais plutôt qu'il renvoie un pointeur vers un objet région de stockage suffisamment grande pour contenir un objet de la taille donnée.

Parce que 6.5p6 utilise le terme "objet" d'une manière qui est contraire à son utilisation partout ailleurs, sa signification dépendra de la manière dont on choisira de définir le terme. En l'absence de la note de bas de page 87 («Les objets alloués n'ont pas de type déclaré»), on pourrait résoudre ce problème en observant simplement que le type effectif de chaque objet est simplement son type. Cela fonctionnerait très bien si l'on reconnaissait les régions de stockage comme contenant une superposition de tous les objets qui pourraient y tenir, mais interprétait la note de bas de page 88 de 6.5p7 ("Le but de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou peut not be alias. ") comme disant que les seuls" objets "auxquels la règle s'applique sont ceux qui sont utilisés dans le même contexte qu'une lvalue sans avoir été récemment et visiblement utilisés dans la dérivation de cette lvalue.

Dans l'état actuel des choses, cependant, la note de bas de page 87 indique clairement que 6.5p6 doit utiliser une signification différente de "objet" par rapport à tout le reste de la Norme, sans préciser quelle est cette signification. Je ne pense pas qu'il soit possible de formuler une définition qui traite raisonnablement tous les cas de coin, et il semble douteux que les auteurs de la norme aient eu un consensus sur ce que les choses étaient ou n'étaient pas des «objets» aux fins de 6.5p6 ou 6.5p7 . Par conséquent, la signification de 6.5p6 et 6.5p7, et ce qui sera permis en fonction de celles-ci, dépendra en grande partie de la manière dont un lecteur choisit de contourner la signification de "objet".


0 commentaires