Kopieren von Strukturen mit nicht initialisierten Mitgliedern

29

Ist es gültig, eine Struktur zu kopieren, deren Mitglieder nicht initialisiert sind?

Ich vermute, dass es sich um ein undefiniertes Verhalten handelt, aber wenn dies der Fall ist, ist es sehr gefährlich, nicht initialisierte Mitglieder in einer Struktur zu belassen (auch wenn diese Mitglieder niemals direkt verwendet werden). Ich frage mich also, ob der Standard etwas enthält, das dies zulässt.

Ist das zum Beispiel gültig?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
quelle
Ich erinnere mich, dass ich vor einiger Zeit eine ähnliche Frage gesehen habe, sie aber nicht finden kann. Diese Frage ist verwandt, genau wie diese .
1201ProgramAlarm

Antworten:

23

Ja, wenn das nicht initialisierte Element kein vorzeichenloser schmaler Zeichentyp ist oder das std::byteKopieren einer Struktur, die diesen unbestimmten Wert enthält, mit dem implizit definierten Kopierkonstruktor ein technisch undefiniertes Verhalten ist, wie dies beim Kopieren einer Variablen mit einem unbestimmten Wert desselben Typs der Fall ist von [dcl.init] / 12 .

Dies gilt hier, da der implizit generierte Kopierkonstruktor mit Ausnahme von unions so definiert ist, dass jedes Mitglied einzeln wie durch direkte Initialisierung kopiert wird , siehe [class.copy.ctor] / 4 .

Dies ist auch Gegenstand der aktiven CWG-Ausgabe 2264 .

In der Praxis werden Sie damit allerdings kein Problem haben.

Wenn Sie 100% sicher sein möchten, hat die Verwendung std::memcpyimmer ein genau definiertes Verhalten, wenn der Typ trivial kopierbar ist , auch wenn Mitglieder einen unbestimmten Wert haben.


Abgesehen von diesen Problemen sollten Sie Ihre Klassenmitglieder bei der Erstellung ohnehin immer ordnungsgemäß mit einem bestimmten Wert initialisieren, vorausgesetzt, Sie benötigen für die Klasse keinen trivialen Standardkonstruktor . Sie können dies ganz einfach mit der Standard-Syntax für die Elementinitialisierung tun, um z. B. die Elemente mit einem Wert zu initialisieren:

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

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Nussbaum
quelle
Nun ... diese Struktur ist kein POD (Plain Old Data)? Das heißt, die Mitglieder werden mit Standardwerten initialisiert? Es ist ein Zweifel
Kevin Kouketsu
Ist es in diesem Fall nicht die flache Kopie? Was kann damit schief gehen, wenn nicht auf das nicht initialisierte Mitglied in der kopierten Struktur zugegriffen wird?
TruthSeeker
@ KevinKouketsu Ich habe eine Bedingung für den Fall hinzugefügt, dass ein Trivial- / POD-Typ erforderlich ist.
Walnuss
@TruthSeeker Der Standard besagt, dass es sich um undefiniertes Verhalten handelt. Der Grund, warum es sich im Allgemeinen um undefiniertes Verhalten für (Nichtmitglieds-) Variablen handelt, wird in der Antwort von AndreySemashev erläutert. Grundsätzlich sollen Trap-Darstellungen mit nicht initialisiertem Speicher unterstützt werden. Ob dies für die implizite Kopierkonstruktion von Strukturen gelten soll , ist die Frage des verknüpften CWG-Problems.
Walnuss
@TruthSeeker Der implizite Kopierkonstruktor ist so definiert, dass jedes Mitglied wie durch direkte Initialisierung einzeln kopiert wird. Es ist nicht definiert, die Objektdarstellung wie von zu kopieren memcpy, selbst für trivial kopierbare Typen. Die einzige Ausnahme bilden Gewerkschaften, für die der implizite Kopierkonstruktor die Objektdarstellung wie von kopiert memcpy.
Walnuss
11

Im Allgemeinen ist das Kopieren nicht initialisierter Daten ein undefiniertes Verhalten, da sich diese Daten möglicherweise in einem Überfüllungszustand befinden. Diese Seite zitieren :

Wenn eine Objektdarstellung keinen Wert des Objekttyps darstellt, wird sie als Trap-Darstellung bezeichnet. Der Zugriff auf eine Trap-Darstellung auf eine andere Weise als das Lesen über einen lvalue-Ausdruck des Zeichentyps ist ein undefiniertes Verhalten.

Signalisierungs-NaNs sind für Gleitkommatypen und auf einigen Plattformen Ganzzahlen möglich können Trap - Darstellungen.

Für trivial kopierbare Typen ist es jedoch möglich, memcpydie Rohdarstellung des Objekts zu kopieren. Dies ist sicher, da der Wert des Objekts nicht interpretiert wird und stattdessen die Rohbyte-Sequenz der Objektdarstellung kopiert wird.

Andrey Semashev
quelle
Was ist mit Daten von Typen, für die alle Bitmuster gültige Werte darstellen (z. B. eine 64-Byte-Struktur mit einem unsigned char[64])? Das Behandeln der Bytes einer Struktur als nicht spezifizierte Werte könnte die Optimierung unnötig behindern, aber die Anforderung, dass Programmierer das Array manuell mit nutzlosen Werten füllen müssen, würde die Effizienz noch mehr beeinträchtigen.
Supercat
Das Initialisieren von Daten ist nicht nutzlos, es verhindert UB, unabhängig davon, ob es durch Trap-Darstellungen oder durch die spätere Verwendung nicht initialisierter Daten verursacht wird. Das Nullsetzen von 64 Bytes (1 oder 2 Cache-Zeilen) ist nicht so teuer, wie es scheint. Und wenn Sie große Strukturen haben, in denen es teuer ist, sollten Sie zweimal überlegen, bevor Sie sie kopieren. Und ich bin mir ziemlich sicher, dass Sie sie sowieso irgendwann initialisieren müssen.
Andrey Semashev
Maschinencodeoperationen, die das Verhalten eines Programms möglicherweise nicht beeinflussen können, sind nutzlos. Die Vorstellung, dass jede Aktion, die durch den Standard als UB gekennzeichnet ist, um jeden Preis vermieden werden muss, statt zu sagen, dass [in den Worten des C-Normungsausschusses] UB "Bereiche einer möglichen konformen Spracherweiterung identifiziert", ist vergleichsweise neu. Ich habe zwar keine veröffentlichten Gründe für den C ++ - Standard gesehen, verzichtet jedoch ausdrücklich auf die Zuständigkeit für das, was C ++ - Programme "dürfen", indem es sich weigert, Programme als konform oder nicht konform zu kategorisieren, was bedeutet, dass ähnliche Erweiterungen zulässig sind.
Supercat
-1

In einigen Fällen, wie dem beschriebenen, ermöglicht der C ++ - Standard Compilern, Konstrukte auf die Weise zu verarbeiten, die ihre Kunden am nützlichsten finden, ohne dass das Verhalten vorhersehbar sein muss. Mit anderen Worten, solche Konstrukte rufen "undefiniertes Verhalten" auf. Dies bedeutet jedoch nicht, dass solche Konstrukte "verboten" sein sollen, da der C ++ - Standard ausdrücklich auf die Zuständigkeit darüber verzichtet, was wohlgeformte Programme "dürfen". Obwohl mir kein veröffentlichtes Begründungsdokument für den C ++ - Standard bekannt ist, deutet die Tatsache, dass es undefiniertes Verhalten ähnlich wie C89 beschreibt, darauf hin, dass die beabsichtigte Bedeutung ähnlich ist: "Undefiniertes Verhalten gibt dem Implementierer die Lizenz, bestimmte Programmfehler, die schwierig sind, nicht abzufangen diagnostizieren.

Es gibt viele Situationen, in denen die effizienteste Art, etwas zu verarbeiten, darin besteht, die Teile einer Struktur zu schreiben, um die sich Downstream-Code kümmern wird, während diejenigen weggelassen werden, die Downstream-Code nicht interessieren. Die Forderung, dass Programme alle Mitglieder einer Struktur initialisieren, einschließlich derer, um die sich nichts kümmern wird, würde die Effizienz unnötig beeinträchtigen.

Darüber hinaus gibt es einige Situationen, in denen es möglicherweise am effizientesten ist, wenn sich nicht initialisierte Daten nicht deterministisch verhalten. Zum Beispiel gegeben:

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;
}

Wenn sich der nachgelagerte Code nicht um die Werte von Elementen kümmert x.datoder y.datderen Indizes nicht aufgeführt sind arr, kann der Code wie folgt optimiert werden:

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;
  }
}

Diese Verbesserung der Effizienz wäre nicht möglich, wenn Programmierer temp.datvor dem Kopieren jedes Element explizit schreiben müssten, einschließlich derjenigen, die sich nicht um die nachgelagerten Elemente kümmern würden.

Andererseits gibt es einige Anwendungen, bei denen es wichtig ist, die Möglichkeit eines Datenverlusts zu vermeiden. In solchen Anwendungen kann es nützlich sein, entweder eine Version des Codes zu haben, die instrumentiert ist, um jeden Versuch abzufangen, nicht initialisierten Speicher zu kopieren, ohne zu berücksichtigen, ob nachgeschalteter Code ihn betrachten würde, oder es könnte nützlich sein, eine Implementierungsgarantie für jeden Speicher zu haben deren Inhalt durchgesickert sein könnte, würde auf Null gesetzt oder auf andere Weise mit nicht vertraulichen Daten überschrieben.

Nach allem, was ich sagen kann, unternimmt der C ++ - Standard keinen Versuch zu sagen, dass eines dieser Verhaltensweisen nützlicher als das andere ist, um eine Mandatierung zu rechtfertigen. Ironischerweise kann dieser Mangel an Spezifikation dazu dienen, die Optimierung zu erleichtern. Wenn Programmierer jedoch keine schwachen Verhaltensgarantien ausnutzen können, werden Optimierungen negiert.

Superkatze
quelle
-2

Da alle Mitglieder von Dataprimitiven Typen sind, data2erhalten sie eine exakte "bitweise Kopie" aller Mitglieder von data. Der Wert von data2.bist also genau der gleiche wie der Wert vondata.b . Der genaue Wert von data.bkann jedoch nicht vorhergesagt werden, da Sie ihn nicht explizit initialisiert haben. Dies hängt von den Werten der Bytes in dem Speicherbereich ab, der für die zugewiesen ist data.

ivan.ukr
quelle
Können Sie dies mit einem Verweis auf den Standard unterstützen? Die von @walnut bereitgestellten Links implizieren, dass dies ein undefiniertes Verhalten ist. Gibt es im Standard eine Ausnahme für PODs?
Tomek Czajka
Obwohl das Folgende nicht mit dem Standard verknüpft ist: en.cppreference.com/w/cpp/language/… "TriviallyCopyable-Objekte können kopiert werden, indem ihre Objektdarstellungen manuell kopiert werden, z. B. mit std :: memmove. Alle mit C kompatiblen Datentypen Sprache (POD-Typen) sind trivial kopierbar. "
ivan.ukr
Das einzige "undefinierte Verhalten" in diesem Fall ist, dass wir den Wert einer nicht initialisierten Mitgliedsvariablen nicht vorhersagen können. Der Code wird jedoch kompiliert und erfolgreich ausgeführt.
ivan.ukr
1
Das Fragment, das Sie zitieren, spricht über das Verhalten von memmove, aber es ist hier nicht wirklich relevant, da ich in meinem Code den Kopierkonstruktor verwende, nicht memmove. Die anderen Antworten implizieren, dass die Verwendung des Kopierkonstruktors zu undefiniertem Verhalten führt. Ich denke, Sie verstehen auch den Begriff "undefiniertes Verhalten" falsch. Dies bedeutet, dass die Sprache überhaupt keine Garantien bietet, z. B. könnte das Programm abstürzen oder Daten zufällig beschädigen oder irgendetwas tun. Dies bedeutet nicht nur, dass ein Wert unvorhersehbar ist, sondern auch ein nicht angegebenes Verhalten.
Tomek Czajka
@ ivan.ukr Der C ++ - Standard gibt an, dass die impliziten Kopier- / Verschiebungskonstruktoren wie durch direkte Initialisierung mitgliederweise handeln (siehe Links in meiner Antwort). Daher ist die Kopie Konstruktion nicht eine „make ‚Bit- für -Bit - Kopie‘ “. Sie sind nur dann richtig für Union - Typen, für die der implizite Copykonstruktor wird angegeben , die Objektdarstellung als ob durch eine manuelle zu kopieren std::memcpy. Nichts davon verhindert die Verwendung von std::memcpyoder std::memmove. Es wird nur die Verwendung des impliziten Kopierkonstruktors verhindert.
Walnuss