Qu'est-ce qui empêche le chevauchement des membres adjacents dans les classes?

12

Considérez les trois structs suivants:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Sur les plates-formes typiques où intest de 4 octets, les tailles, les alignements et le remplissage total 1 sont les suivants:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Il n'y a pas de chevauchement entre le stockage des éléments blubet blob, même si la taille 1 blobpourrait en principe "rentrer" dans le rembourrage de blub.

C ++ 20 introduit l' no_unique_addressattribut, qui permet aux membres vides adjacents de partager la même adresse. Il permet également explicitement le scénario décrit ci-dessus d'utiliser le remplissage d'un membre pour en stocker un autre. De cppreference (soulignement le mien):

Indique que ce membre de données n'a pas besoin d'avoir une adresse distincte de tous les autres membres de données non statiques de sa classe. Cela signifie que si le membre a un type vide (par exemple Allocator sans état), le compilateur peut l'optimiser pour n'occuper aucun espace, comme s'il s'agissait d'une base vide. Si le membre n'est pas vide, tout remplissage de queue peut également être réutilisé pour stocker d'autres membres de données.

En effet, si nous utilisons cet attribut sur blub b0la taille des blagouttes 8, le blobest en effet stocké dans le blub comme vu sur godbolt .

Enfin, nous arrivons à ma question:

Quel texte dans les normes (C ++ 11 à C ++ 20) empêche ce chevauchement sans no_unique_address, pour les objets qui ne sont pas trivialement copiables?

J'ai besoin d'exclure les objets trivialement copiables (TC) de ce qui précède, car pour les objets TC, il est autorisé de passer std::memcpyd'un objet à un autre, y compris les sous-objets membres, et si le stockage était chevauché, cela se briserait (car tout ou partie du stockage pour le membre adjacent serait écrasé) 2 .


1 Nous calculons le remplissage simplement comme la différence entre la taille de la structure et la taille de tous ses membres constitutifs, récursivement.

2 Voilà pourquoi j'ai constructeurs de copie définis: pour faire blubet blobnon trivialement copiable .

BeeOnRope
la source
Je n'ai pas fait de recherche, mais je devine la règle du "comme si". S'il n'y a pas de différence observable (un terme avec une signification très spécifique btw) par rapport à la machine abstraite (qui est ce contre quoi votre code est compilé), alors le compilateur peut changer le code comme bon lui semble.
Jesper Juhl
Je suis sûr que c'est une dupe: stackoverflow.com/questions/53837373/…
NathanOliver
@JesperJuhl - à droite, mais je demande pourquoi cela ne peut pas , pas pourquoi , et la règle "comme si" s'applique généralement aux premiers mais n'a pas de sens pour les seconds. De plus, "comme si" n'est pas clair pour la disposition de la structure qui est généralement une préoccupation globale, pas locale. En fin de compte, le compilateur doit avoir un seul ensemble cohérent de règles de mise en page, sauf peut-être pour les structures qu'il ne peut jamais "s'échapper".
BeeOnRope
1
@BeeOnRope Je ne peux pas répondre à votre question, désolé. C'est pourquoi je viens de poster un commentaire et non une réponse. Ce que vous avez obtenu dans ce commentaire était ma meilleure supposition vers une explication, mais je ne connais pas la réponse (curieux de l'apprendre moi-même - c'est pourquoi vous avez obtenu un vote positif).
Jesper Juhl
1
@NicolBolas - répondez-vous à la bonne question? Il ne s'agit pas de détecter des copies sécurisées ou autre chose. Je suis plutôt curieux de savoir pourquoi le rembourrage ne peut pas être réutilisé entre les membres. Dans tous les cas, vous vous trompez: trivialement copiable est une propriété du type et l'a toujours été. Cependant, pour copier un objet en toute sécurité, il doit à la fois avoir un type TC (une propriété du type) et ne pas être un sujet potentiellement chevauchant (une propriété de l'objet, ce qui, je suppose, est l'endroit où vous vous êtes trompé). Je ne sais toujours pas pourquoi nous parlons ici de copies.
BeeOnRope

Réponses:

1

Le standard est terriblement silencieux quand on parle du modèle de mémoire et pas très explicite sur certains des termes qu'il utilise. Mais je pense avoir trouvé une argumentation de travail (qui peut être un peu faible)

Voyons d'abord ce qui fait même partie d'un objet. [types.base] / 4 :

La représentation objet d'un objet de type Test la séquence d' N unsigned charobjets absorbés par l'objet de type T, où Nest égal sizeof(T). La représentation de valeur d'un objet de type Test l'ensemble de bits qui participent à la représentation d'une valeur de type T. Les bits de la représentation d'objet qui ne font pas partie de la représentation de valeur sont des bits de remplissage.

Ainsi, la représentation des objets se b0compose d' sizeof(blub) unsigned charobjets, soit 8 octets. Les bits de remplissage font partie de l'objet.

Aucun objet ne peut occuper l'espace d'un autre s'il n'est pas imbriqué en lui [basic.life] /1.5 :

La durée de vie d'un objet ode type Tse termine lorsque:

[...]

(1.5) le stockage occupé par l'objet est libéré ou est réutilisé par un objet qui n'est pas imbriqué dans o([intro.object]).

Ainsi, la durée de vie de b0se terminerait, lorsque le stockage occupé par celui-ci serait réutilisé par un autre objet, c'est-à-dire b1. Je n'ai pas vérifié cela, mais je pense que la norme exige que le sous-objet d'un objet vivant soit également vivant (et je ne pouvais pas imaginer comment cela devrait fonctionner différemment).

Ainsi, le stockage qui b0 occupe ne peut pas être utilisé par b1. Je n'ai trouvé aucune définition de «occuper» dans la norme, mais je pense qu'une interprétation raisonnable ferait «partie de la représentation de l'objet». Dans la représentation d'objet décrivant le devis, les mots "reprendre" sont utilisés 1 . Ici, ce serait 8 octets, donc il en blafaut au moins un de plus pour b1.

Surtout pour les sous-objets (donc entre autres membres de données non statiques) il y a aussi la stipulation [intro.object] / 9 (mais cela a été ajouté avec C ++ 20, thx @BeeOnRope)

Deux objets dont les durées de vie se chevauchent et qui ne sont pas des champs binaires peuvent avoir la même adresse si l'un est imbriqué dans l'autre, ou si au moins l'un est un sous-objet de taille nulle et ils sont de types différents; sinon, ils ont des adresses distinctes et occupent des octets de stockage disjoints .

(soulignement le mien) Ici encore, nous avons le problème que "occupe" n'est pas défini et je dirais encore une fois de prendre les octets dans la représentation de l'objet. Notez qu'il y a une note de bas de page [basic.memobj] / note de bas de page 29

En vertu de la règle «comme si», une implémentation est autorisée à stocker deux objets à la même adresse machine ou à ne pas stocker du tout d'objet si le programme ne peut pas observer la différence ([intro.execution]).

Ce qui peut permettre au compilateur de casser cela s'il peut prouver qu'il n'y a pas d'effet secondaire observable. Je pense que c'est assez compliqué pour une chose aussi fondamentale que la disposition des objets. C'est peut-être la raison pour laquelle cette optimisation n'est prise que lorsque l'utilisateur fournit les informations qu'il n'y a aucune raison d'avoir des objets disjoints en ajoutant l' [no_unique_address]attribut.

tl; dr: le remplissage peut faire partie de l'objet et les membres doivent être disjoints.


1 Je n'ai pas pu résister à l'ajout d'une référence qui pourrait signifier occuper: Webster's Revised Unabridged Dictionary, G. & C. Merriam, 1913 (c'est moi qui souligne)

  1. Pour maintenir ou remplir les dimensions de; occuper la salle ou l'espace de; couvrir ou remplir; comme, le camp occupe cinq acres de terrain. Sir J. Herschel.

Quelle analyse standard serait complète sans analyse de dictionnaire?

n314159
la source
2
La partie «occupy disjoint bytes of storage» de into.storage me suffirait, je pense, mais cette formulation n'a été ajoutée qu'en C ++ 20 dans le cadre de la modification qui a été ajoutée no_unique_address. Cela laisse la situation avant C ++ 20 moins claire. Je n'ai pas compris votre raisonnement conduisant à "Aucun objet ne peut occuper l'espace d'un autre s'il n'est pas imbriqué" à partir de basic.life/1.5, en particulier comment obtenir de "le stockage que l'objet occupe est libéré" à "aucun objet ne peut occuper l'espace d'un autre".
BeeOnRope
1
J'ai ajouté une petite précision à ce paragraphe. J'espère que cela le rend plus compréhensible. Sinon, je le reverrai demain, en ce moment il est assez tard pour moi.
n314159
"Deux objets avec des durées de vie qui se chevauchent qui ne sont pas des champs binaires peuvent avoir la même adresse si l'un est imbriqué dans l'autre, ou si au moins l'un est un sous-objet de taille nulle et ils sont de types différents" 2 objets avec des durées de vie qui se chevauchent, de du même type, ont la même adresse .
Avocat en langues le
Désolé, pourriez-vous élaborer? Vous citez une citation standard de ma réponse et apportez un exemple qui entre un peu en conflit avec cela. Je ne sais pas s'il s'agit d'un commentaire sur ma réponse et si c'est ce qu'elle devrait me dire. En ce qui concerne votre exemple, je dirais qu'il fallait prendre en compte d'autres parties de la norme (il y a un paragraphe sur un tableau de caractères non signé fournissant du stockage pour un autre objet, quelque chose concernant l'optimisation de base de taille zéro et encore plus, on devrait également regarder si le placement nouveau a allocations spéciales, toutes choses qui ne me
paraissent
@ n314159 Je pense que ce libellé est peut-être défectueux.
Law Lawyer