Cosa impedisce la sovrapposizione dei membri adiacenti nelle classi?


12

Considera le seguenti tre structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Su piattaforme tipiche in cui intsono presenti 4 byte, le dimensioni, gli allineamenti e il riempimento totale 1 sono i seguenti:

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

Non vi è alcuna sovrapposizione tra la memoria dei membri blube blob, anche se la dimensione 1 blobpotrebbe in linea di principio "adattarsi" all'imbottitura di blub.

C ++ 20 introduce l' no_unique_addressattributo, che consente ai membri vuoti adiacenti di condividere lo stesso indirizzo. Inoltre, consente esplicitamente lo scenario sopra descritto di utilizzare il riempimento di un membro per memorizzarne un altro. Da cppreference (sottolineatura mia):

Indica che questo membro di dati non deve necessariamente avere un indirizzo distinto da tutti gli altri membri di dati non statici della sua classe. Ciò significa che se il membro ha un tipo vuoto (ad es. Allocatore senza stato), il compilatore può ottimizzarlo per non occupare spazio, proprio come se fosse una base vuota. Se il membro non è vuoto, è possibile riutilizzare qualsiasi padding di coda per memorizzare altri membri di dati.

In effetti, se usiamo questo attributo su blub b0, la dimensione di blascende a 8, quindi blobè effettivamente memorizzato nel blub come visto su godbolt .

Infine, arriviamo alla mia domanda:

Quale testo negli standard (da C ++ 11 a C ++ 20) impedisce questa sovrapposizione senza no_unique_address, per oggetti che non sono banalmente copiabili?

Devo escludere oggetti banalmente copiabili (TC) da quanto sopra, perché per gli oggetti TC, è consentito passare std::memcpyda un oggetto a un altro, compresi gli oggetti secondari dei membri, e se la memoria fosse sovrapposta si spezzerebbe (perché tutto o parte della memoria poiché il membro adiacente verrebbe sovrascritto) 2 .


1 Calcoliamo l'imbottitura semplicemente come la differenza tra la dimensione della struttura e la dimensione di tutti i suoi componenti, in modo ricorsivo.

2 Questo è il motivo per cui ho definito i costruttori di copie: da rendere blube blobnon banalmente copiabili .


Non l'ho studiato, ma sto indovinando la regola "come se". Se non vi è alcuna differenza osservabile (un termine con un significato molto specifico tra parentesi) rispetto alla macchina astratta (che è ciò contro cui viene compilato il codice), il compilatore può modificare il codice come preferisce.
Jesper Juhl,

Abbastanza sicuro che questa è una vittima di questo: stackoverflow.com/questions/53837373/...
NathanOliver

@JesperJuhl - giusto, ma sto chiedendo perché non può , non perché , e la regola "come se" di solito si applica alla prima, ma non ha senso per la seconda. Inoltre, "come se" non fosse chiaro per il layout della struttura che di solito è un problema globale, non locale. Alla fine il compilatore deve avere un unico insieme coerente di regole per il layout, tranne forse per le strutture che non può mai dimostrare di "sfuggire".
BeeOnRope,

1
@BeeOnRope Non posso rispondere alla tua domanda, scusa. Ecco perché ho appena pubblicato un commento e non una risposta. Quello che hai ottenuto in quel commento è stata la mia migliore ipotesi verso una spiegazione, ma non conosco la risposta (è curioso di impararla da solo - ecco perché hai ottenuto un voto).
Jesper Juhl,

1
@NicolBolas - stai rispondendo alla domanda giusta? Non si tratta di rilevare copie sicure o altro. Piuttosto sono curioso di sapere perché l'imbottitura non può essere riutilizzata tra i membri. In ogni caso, ti sbagli: banalmente copiabile è una proprietà del tipo e lo è sempre stata. Tuttavia, per copiare in modo sicuro un oggetto, entrambi devono avere un tipo TC (una proprietà del tipo) e non essere un soggetto potenzialmente sovrapposto (una proprietà dell'oggetto, che immagino sia dove ti sei confuso). Non so ancora perché stiamo parlando di copie qui.
BeeOnRope,

Risposte:


1

Lo standard è terribilmente silenzioso quando si parla del modello di memoria e non molto esplicito su alcuni dei termini che utilizza. Ma penso di aver trovato un'argomentazione funzionante (che potrebbe essere un po 'debole)

Innanzitutto, scopriamo cosa fa parte anche di un oggetto. [basic.types] / 4 :

La rappresentazione dell'oggetto di un oggetto di tipo Tè la sequenza di N unsigned charoggetti occupata dall'oggetto di tipo T, dove è Nuguale sizeof(T). La rappresentazione del valore di un oggetto di tipo Tè l'insieme di bit che partecipano alla rappresentazione di un valore di tipo T. I bit nella rappresentazione dell'oggetto che non fanno parte della rappresentazione del valore sono bit di riempimento.

Quindi la rappresentazione b0degli sizeof(blub) unsigned charoggetti è costituita da oggetti, quindi 8 byte. I bit di riempimento fanno parte dell'oggetto.

Nessun oggetto può occupare lo spazio di un altro se non è nidificato al suo interno [basic.life] /1.5 :

La durata di un oggetto odi tipo Ttermina quando:

[...]

(1.5) la memoria ooccupata dall'oggetto viene rilasciata o riutilizzata da un oggetto che non è nidificato all'interno ([intro.object]).

Quindi la vita di b0sarebbe finita, quando la memoria che è occupata da essa sarebbe riutilizzata da un altro oggetto, vale a dire b1. Non l'ho verificato, ma penso che i mandati standard prevedano che anche l'oggetto secondario di un oggetto che è vivo (e non potrei immaginare come dovrebbe funzionare diversamente).

Quindi l'archiviazione che b0 occupa non può essere utilizzata da b1. Non ho trovato alcuna definizione di "occupare" nello standard, ma penso che un'interpretazione ragionevole sarebbe "parte della rappresentazione dell'oggetto". Nella citazione che descrive la rappresentazione dell'oggetto, vengono utilizzate le parole "take up" 1 . Qui, questo sarebbe di 8 byte, quindi ne blanecessita almeno uno in più per b1.

Soprattutto per gli oggetti secondari (quindi tra gli altri membri di dati non statici) c'è anche la clausola [intro.object] / 9 (ma questo è stato aggiunto con C ++ 20, thx @BeeOnRope)

Due oggetti con durata di vita sovrapposta che non sono campi di bit possono avere lo stesso indirizzo se uno è nidificato all'interno dell'altro o se almeno uno è un oggetto secondario di dimensione zero e sono di tipo diverso; in caso contrario, hanno indirizzi distinti e occupano byte di memoria disgiunti .

(enfasi mia) Anche qui abbiamo il problema che "occupa" non è definito e di nuovo direi di prendere i byte nella rappresentazione dell'oggetto. Nota che c'è una nota a piè di pagina in questo [basic.memobj] / nota 29

In base alla regola "as-if", a un'implementazione è consentito memorizzare due oggetti allo stesso indirizzo macchina o non memorizzare affatto un oggetto se il programma non è in grado di osservare la differenza ([intro.execution]).

Ciò può consentire al compilatore di interrompere questo se può dimostrare che non vi sono effetti collaterali osservabili. Penso che questo sia piuttosto complicato per una cosa così fondamentale come il layout degli oggetti. Forse è per questo che questa ottimizzazione viene presa solo quando l'utente fornisce le informazioni che non vi è alcun motivo per avere oggetti disgiunti aggiungendo l' [no_unique_address]attributo.

tl; dr: Il riempimento potrebbe essere parte dell'oggetto e i membri devono essere disgiunti.


1 Non ho resistito all'aggiunta di un riferimento che occupare potrebbe voler dire riprendere: Webster's Revised Unabridged Dictionary, G. & C. Merriam, 1913 (sottolineatura mia)

  1. Per contenere o riempire le dimensioni di; occupare la stanza o lo spazio di; per coprire o riempire; come, il campo occupa cinque acri di terra. Sir J. Herschel.

Quale ricerca per indicizzazione standard sarebbe completa senza una ricerca per indicizzazione del dizionario?


2
La parte "occupare i byte disgiunti di archiviazione" da into.storage sarebbe sufficiente, credo, per me - ma questa formulazione è stata aggiunta in C ++ 20 solo come parte della modifica aggiunta no_unique_address. Lascia la situazione precedente a C ++ 20 meno chiara. Non ho capito il tuo ragionamento che porta a "Nessun oggetto può occupare lo spazio di un altro se non è un nidificato al suo interno" da basic.life/1.5, in particolare come ottenere da "l'archivio che occupa l'oggetto viene rilasciato" a "nessun oggetto può occupare lo spazio di un altro".
BeeOnRope,

1
Ho aggiunto un piccolo chiarimento a quel paragrafo. Spero che questo lo renda più comprensibile. Altrimenti lo guarderò di nuovo domani, adesso è abbastanza tardi per me.
n314159,

"Due oggetti con durata sovrapposta che non sono campi di bit possono avere lo stesso indirizzo se uno è nidificato all'interno dell'altro o se almeno uno è un oggetto secondario di dimensione zero e sono di tipo diverso" 2 oggetti con durata sovrapposta, di lo stesso tipo, hanno lo stesso indirizzo .
Avvocato linguistico il

Scusa, potresti approfondire? Stai citando una citazione standard dalla mia risposta e stai portando un esempio in conflitto con quello. Non sono sicuro se questo è un commento sulla mia risposta e se è quello che dovrebbe dirmi. Per quanto riguarda il tuo esempio, direi che bisogna considerare ancora altre parti dello standard (c'è un paragrafo su un array di caratteri non firmato che fornisce spazio di archiviazione per un altro oggetto, qualcosa che riguarda l'ottimizzazione della base di dimensioni zero e inoltre si dovrebbe anche cercare se il posizionamento nuovo ha indennità speciali, tutte le cose che non credo siano rilevanti per l'esempio dei PO)
n314159

@ n314159 Penso che questa formulazione potrebbe essere difettosa.
Avvocato linguistico il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.