Copia di strutture con membri non inizializzati


29

È valido copiare una struttura di cui alcuni membri non sono inizializzati?

Ho il sospetto che si tratti di un comportamento indefinito, ma in tal caso, rende piuttosto pericoloso lasciare membri non inizializzati in una struttura (anche se quei membri non vengono mai usati direttamente). Quindi mi chiedo se c'è qualcosa nello standard che lo consente.

Ad esempio, è valido?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}

Ricordo di aver visto una domanda simile qualche tempo fa, ma non riesco a trovarla. Questa domanda è correlata come questa .
1201Program Allarme

Risposte:


23

Sì, se il membro non inizializzato non è un tipo di carattere stretto senza segno o std::byte, quindi copiare una struttura contenente questo valore indeterminato con il costruttore di copia implicitamente definito è un comportamento tecnicamente indefinito, come lo è per copiare una variabile con valore indeterminato dello stesso tipo, perché di [dcl.init] / 12 .

Questo si applica qui, perché il costruttore di copie generato implicitamente è, tranne per unions, definito per copiare ogni membro individualmente come se inizializzasse direttamente, vedere [class.copy.ctor] / 4 .

Questo è anche argomento del numero CWG attivo 2264 .

Suppongo che in pratica non avrai alcun problema a riguardo.

Se vuoi essere sicuro al 100%, l'utilizzo std::memcpyha sempre un comportamento ben definito se il tipo è banalmente copiabile , anche se i membri hanno un valore indeterminato.


A parte questi problemi, dovresti sempre inizializzare correttamente i membri della tua classe con un valore specificato in fase di costruzione, supponendo che non sia necessario che la classe abbia un costruttore predefinito banale . Puoi farlo facilmente usando la sintassi di inizializzazione del membro predefinita per esempio inizializzare il valore dei membri:

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}

bene .. che struct non è un POD (dati semplici vecchi)? Ciò significa che i membri verranno inizializzati con valori predefiniti? È un dubbio
Kevin Kouketsu il

Non è la copia superficiale in questo caso? cosa può andare storto con questo se non si accede al membro non inizializzato nella struttura copiata?
TruthSeeker

@KevinKouketsu Ho aggiunto una condizione per il caso in cui è richiesto un tipo banale / POD.
noce

@TruthSeeker Lo standard afferma che si tratta di un comportamento indefinito. Il motivo per cui è generalmente un comportamento indefinito per le variabili (non membro) è spiegato nella risposta di AndreySemashev. Fondamentalmente è per supportare rappresentazioni di trap con memoria non inizializzata. Se questo è destinato ad applicarsi alla costruzione implicita di copie di strutture è la questione del problema CWG collegato.
noce

@TruthSeeker Il costruttore di copia implicita è definito per copiare ogni membro individualmente come se fosse l'inizializzazione diretta. Non è definito per copiare la rappresentazione dell'oggetto come se fosse memcpy, anche per tipi banalmente copiabili. L'unica eccezione sono i sindacati, per i quali il costruttore di copia implicita copia la rappresentazione dell'oggetto come se fosse di memcpy.
noce

11

In generale, la copia di dati non inizializzati è un comportamento indefinito perché tali dati potrebbero trovarsi in uno stato di trapping. Citando questa pagina:

Se una rappresentazione di oggetto non rappresenta alcun valore del tipo di oggetto, è nota come rappresentazione trap. L'accesso a una rappresentazione trap in qualsiasi modo diverso dalla lettura attraverso un'espressione lvalue di tipo carattere è un comportamento indefinito.

I NaN di segnalazione sono possibili per i tipi a virgola mobile e su alcune piattaforme gli interi possono avere rappresentazioni di trap.

Tuttavia, per tipi banalmente copiabili è possibile utilizzare memcpyper copiare la rappresentazione grezza dell'oggetto. Ciò è sicuro poiché il valore dell'oggetto non viene interpretato e viene copiata la sequenza di byte non elaborati della rappresentazione dell'oggetto.


Che dire dei dati di tipi per i quali tutti i modelli di bit rappresentano valori validi (ad es. Una struttura a 64 byte contenente un unsigned char[64])? Trattare i byte di una struttura come valori non specificati potrebbe inutilmente impedire l'ottimizzazione, ma richiedere ai programmatori di popolare manualmente l'array con valori inutili impedirebbe ancora di più l'efficienza.
Supercat

L'inizializzazione dei dati non è inutile, impedisce UB, sia che sia causato da rappresentazioni trap o utilizzando dati non inizializzati in seguito. L'azzeramento di 64 byte (1 o 2 righe della cache) non è così costoso come potrebbe sembrare. E se hai strutture di grandi dimensioni dove è costoso, dovresti pensarci due volte prima di copiarle. E sono abbastanza sicuro che dovrete inizializzarli comunque ad un certo punto.
Andrey Semashev,

Le operazioni del codice macchina che non possono influire sul comportamento di un programma sono inutili. L'idea che qualsiasi azione caratterizzata come UB dalla norma debba essere evitata a tutti i costi, piuttosto dicendo che [nelle parole del Comitato per le norme C] UB "identifica le aree di estensione del linguaggio conforme", è relativamente recente. Anche se non ho visto una motivazione pubblicata per lo standard C ++, esso rinuncia espressamente alla giurisdizione su ciò che i programmi C ++ sono "autorizzati" a fare rifiutando di classificare i programmi come conformi o non conformi, il che significa che consentirebbe estensioni simili.
supercat

-1

In alcuni casi, come quello descritto, lo standard C ++ consente ai compilatori di elaborare i costrutti in qualunque modo i loro clienti troverebbero più utili, senza richiedere che il comportamento sia prevedibile. In altre parole, tali costrutti invocano "Undefined Behaviour". Ciò non implica, tuttavia, che tali costrutti debbano essere "vietati" poiché lo standard C ++ rinuncia esplicitamente alla giurisdizione su ciò che i "programmi ben formati" sono "autorizzati" a fare. Sebbene non sia a conoscenza di alcun documento di Rationale pubblicato per lo standard C ++, il fatto che descriva un comportamento indefinito come C89 suggerirebbe che il significato previsto è simile: "Il comportamento indefinito dà alla licenza dell'attore di non rilevare alcuni errori del programma che sono difficili diagnosticare.

Ci sono molte situazioni in cui il modo più efficiente di elaborare qualcosa implicherebbe la scrittura delle parti di una struttura di cui il codice a valle si preoccuperà, omettendo quelle a cui il codice a valle non importa. Richiedere che i programmi inizializzino tutti i membri di una struttura, compresi quelli di cui nulla si preoccuperà mai, impedirebbe inutilmente l'efficienza.

Inoltre, ci sono alcune situazioni in cui può essere più efficiente avere dati non inizializzati comportarsi in modo non deterministico. Ad esempio, dato:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

se al codice downstream non interessano i valori di alcun elemento x.dato di y.datcui non sono stati elencati gli indici arr, il codice potrebbe essere ottimizzato per:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Questo miglioramento dell'efficienza non sarebbe possibile se ai programmatori fosse richiesto di scrivere esplicitamente ogni elemento temp.dat, compresi quelli a valle, se ne fregerebbe, prima di copiarlo.

D'altra parte, ci sono alcune applicazioni in cui è importante evitare la possibilità di perdita di dati. In tali applicazioni, può essere utile disporre di una versione del codice che sia strumentata per intercettare qualsiasi tentativo di copiare l'archiviazione non inizializzata senza tener conto del fatto che il codice downstream lo guarderebbe, oppure potrebbe essere utile disporre di una garanzia di implementazione che qualsiasi archiviazione i cui contenuti potrebbero essere trapelati verrebbero azzerati o altrimenti sovrascritti con dati non riservati.

Da quello che posso dire, lo standard C ++ non fa alcun tentativo di dire che uno di questi comportamenti è sufficientemente più utile dell'altro da giustificarlo. Ironia della sorte, questa mancanza di specifiche può essere intesa a facilitare l'ottimizzazione, ma se i programmatori non possono sfruttare alcun tipo di garanzie comportamentali deboli, eventuali ottimizzazioni verranno annullate.


-2

Dal momento che tutti i membri di Datasono di tipi primitivi, data2otterrà l'esatta "copia bit per bit" di tutti i membri di data. Quindi il valore di data2.bsarà esattamente uguale al valore di data.b. Tuttavia, data.bnon è possibile prevedere il valore esatto di , poiché non è stato inizializzato esplicitamente. Dipenderà dai valori dei byte nell'area di memoria allocata per data.


Potete supportarlo con un riferimento allo standard? I collegamenti forniti da @walnut implicano che si tratta di un comportamento indefinito. Esiste un'eccezione per i POD nello standard?
Tomek Czajka,

Sebbene quanto segue non sia link allo standard, comunque: en.cppreference.com/w/cpp/language/… "Gli oggetti TriviallyCopyable possono essere copiati copiando manualmente le loro rappresentazioni di oggetti, ad es. Con std :: memmove. Tutti i tipi di dati compatibili con la C la lingua (tipi POD) è banalmente copiabile. "
ivan.ukr,

L'unico "comportamento indefinito" in questo caso è che non possiamo prevedere il valore della variabile membro non inizializzata, ma il codice viene compilato ed eseguito correttamente.
ivan.ukr,

1
Il frammento che citi parla del comportamento di memmove, ma non è molto rilevante qui perché nel mio codice sto usando il costruttore di copie, non memmove. Le altre risposte implicano che l'uso del costruttore di copie comporta un comportamento indefinito. Penso che tu abbia anche frainteso il termine "comportamento indefinito". Significa che la lingua non fornisce alcuna garanzia, ad esempio il programma potrebbe arrestarsi in modo anomalo o corrompere i dati in modo casuale o fare qualsiasi cosa. Non significa solo che un certo valore è imprevedibile, sarebbe un comportamento non specificato.
Tomek Czajka,

@ ivan.ukr Lo standard C ++ specifica che i costruttori impliciti di copia / spostamento agiscono come membri come se inizializzassero direttamente, vedere i collegamenti nella mia risposta. Pertanto la costruzione della copia non crea una " " copia bit per bit " ". Sei corretto solo per i tipi di unione, per i quali viene specificato il costruttore di copia implicita per copiare la rappresentazione dell'oggetto come se fosse un manuale std::memcpy. Niente di tutto ciò impedisce di usare std::memcpyo std::memmove. Impedisce solo l'uso del costruttore di copie implicite.
noce,
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.