Von http://en.cppreference.com/w/cpp/string/byte/memcpy :
Wenn die Objekte nicht TriviallyCopyable sind (z. B. Skalare, Arrays, C-kompatible Strukturen), ist das Verhalten undefiniert.
Bei meiner Arbeit haben wir std::memcpy
lange Zeit Objekte, die nicht TriviallyCopyable sind, bitweise ausgetauscht, indem wir :
void swapMemory(Entity* ePtr1, Entity* ePtr2)
{
static const int size = sizeof(Entity);
char swapBuffer[size];
memcpy(swapBuffer, ePtr1, size);
memcpy(ePtr1, ePtr2, size);
memcpy(ePtr2, swapBuffer, size);
}
und hatte nie irgendwelche Probleme.
Ich verstehe, dass es trivial ist, std::memcpy
mit nicht TriviallyCopyable-Objekten zu missbrauchen und stromabwärts undefiniertes Verhalten zu verursachen. Meine Frage:
Warum sollte das Verhalten von sich std::memcpy
selbst undefiniert sein, wenn es mit nicht TriviallyCopyable-Objekten verwendet wird? Warum hält es der Standard für notwendig, dies anzugeben?
AKTUALISIEREN
Der Inhalt von http://en.cppreference.com/w/cpp/string/byte/memcpy wurde als Antwort auf diesen Beitrag und die Antworten auf den Beitrag geändert. Die aktuelle Beschreibung lautet:
Wenn die Objekte nicht TriviallyCopyable sind (z. B. Skalare, Arrays, C-kompatible Strukturen), ist das Verhalten undefiniert, es sei denn, das Programm hängt nicht von den Auswirkungen des Destruktors des Zielobjekts (das nicht ausgeführt wird
memcpy
) und der Lebensdauer des Das Zielobjekt (das beendet, aber nicht gestartet wirdmemcpy
) wird auf andere Weise gestartet, z. B. durch Platzierung neu.
PS
Kommentar von @Cubbi:
@RSahu Wenn etwas UB Downstream garantiert, wird das gesamte Programm undefiniert. Ich stimme jedoch zu, dass es in diesem Fall möglich zu sein scheint, UB zu umgehen und die Referenz entsprechend zu ändern.
T
, wenn zwei Zeiger aufT
unterschiedlicheT
Objekte verweisenobj1
undobj2
wenn weder ein Unterobjekt der Basisklasseobj1
nochobj2
ein Basisobjekt ist, wenn die zugrunde liegenden Bytes, aus denenobj1
sich zusammensetzt, kopiert werdenobj2
,obj2
muss anschließend das gleicher Wert wieobj1
". (Hervorhebung von mir) Das nachfolgende Beispiel verwendetstd::memcpy
.Antworten:
Es ist nicht! Sobald Sie jedoch die zugrunde liegenden Bytes eines Objekts eines nicht trivial kopierbaren Typs in ein anderes Objekt dieses Typs kopieren, ist das Zielobjekt nicht mehr aktiv . Wir haben es durch Wiederverwendung seines Speichers zerstört und es nicht durch einen Konstruktoraufruf wiederbelebt.
Die Verwendung des Zielobjekts - Aufrufen seiner Elementfunktionen , Zugreifen auf seine Datenelemente - ist eindeutig undefiniert [basic.life] / 6 , ebenso wie ein nachfolgender impliziter Destruktoraufruf [basic.life] / 4 für Zielobjekte mit automatischer Speicherdauer. Beachten Sie, wie undefiniert das Verhalten rückwirkend ist . [intro.execution] / 5:
Wenn eine Implementierung erkennt, wie ein Objekt tot ist und notwendigerweise weiteren Operationen unterzogen wird, die nicht definiert sind, ... kann sie darauf reagieren, indem sie die Semantik Ihres Programms ändert. Ab dem
memcpy
Anruf. Und diese Überlegung wird sehr praktisch, wenn wir an Optimierer und bestimmte Annahmen denken, die sie treffen.Es ist jedoch zu beachten, dass Standardbibliotheken bestimmte Standardbibliotheksalgorithmen für trivial kopierbare Typen optimieren können und dürfen.
std::copy
Bei Zeigern auf trivial kopierbare Typen werden normalerweisememcpy
die zugrunde liegenden Bytes aufgerufen. Das tut es auchswap
.Halten Sie sich also einfach an die Verwendung normaler generischer Algorithmen und lassen Sie den Compiler alle geeigneten Optimierungen auf niedriger Ebene durchführen. Dies ist teilweise der Grund, warum die Idee eines trivial kopierbaren Typs erfunden wurde: Feststellung der Rechtmäßigkeit bestimmter Optimierungen. Dies vermeidet auch, Ihr Gehirn zu verletzen, indem Sie sich um widersprüchliche und nicht spezifizierte Teile der Sprache sorgen müssen.
quelle
memcpy
die Lebensdauer des Zielobjekts mit einem solchen Typ endet, wurde es nicht wiederbelebt. Dies steht im Widerspruch zu Ihrer Argumentation, denke ich (obwohl es eine Inkonsistenz im Standard selbst sein könnte).memcpy
soll als Wiederverwendung gelten. Die Trivialität von init (oder Leere ) ist eine Eigenschaft des init, nicht des Typs. Es gibt keine Init über ctor des Zielobjekts, wennmemcpy
, daher ist die Init immer leerEs ist einfach genug, eine Klasse zu
memcpy
erstellen, in der das basiertswap
:struct X { int x; int* px; // invariant: always points to x X() : x(), px(&x) {} X(X const& b) : x(b.x), px(&x) {} X& operator=(X const& b) { x = b.x; return *this; } };
memcpy
Ein solches Objekt bricht diese Invariante.GNU C ++ 11
std::string
macht genau das mit kurzen Strings.Dies ähnelt der Implementierung der Standarddatei- und Zeichenfolgenströme. Die Streams werden schließlich abgeleitet, von
std::basic_ios
denen ein Zeiger auf enthältstd::basic_streambuf
. Die Streams enthalten auch den spezifischen Puffer als Element (oder Basisklassen-Unterobjekt), auf den dieser Zeiger instd::basic_ios
zeigt.quelle
memcpy
in solchen Fällen einfach die Invariante gebrochen wird , aber die Effekte sind streng definiert (rekursivmemcpy
s die Mitglieder, bis sie trivial kopierbar sind).Weil der Standard es sagt.
Compiler können davon ausgehen, dass nicht TriviallyCopyable-Typen nur über ihre Kopier- / Verschiebungskonstruktoren / Zuweisungsoperatoren kopiert werden. Dies kann zu Optimierungszwecken erfolgen (wenn einige Daten privat sind, kann die Einstellung verschoben werden, bis ein Kopieren / Verschieben erfolgt).
Dem Compiler steht es sogar frei, Ihren
memcpy
Anruf anzunehmen und nichts zu tun oder Ihre Festplatte zu formatieren. Warum? Weil der Standard es sagt. Und nichts zu tun ist definitiv schneller als Teile zu bewegen. Warum also nicht Ihrmemcpy
Programm auf ein ebenso gültiges schnelleres Programm optimieren ?In der Praxis gibt es viele Probleme, die auftreten können, wenn Sie nur Bits in Typen herumblitzen, die dies nicht erwarten. Virtuelle Funktionstabellen sind möglicherweise nicht richtig eingerichtet. Instrumente zur Erkennung von Lecks sind möglicherweise nicht richtig eingerichtet. Objekte, deren Identität ihren Standort enthält, werden durch Ihren Code völlig durcheinander gebracht.
Der wirklich lustige Teil ist, dass
using std::swap; swap(*ePtr1, *ePtr2);
es möglich sein sollte,memcpy
vom Compiler auf ein für trivial kopierbare Typen kompiliertes und für andere Typen definiertes Verhalten zu reduzieren. Wenn der Compiler nachweisen kann, dass es sich bei der Kopie nur um kopierte Bits handelt, kann er diese frei ändernmemcpy
. Und wenn Sie ein optimaleres schreiben könnenswap
, können Sie dies im Namespace des betreffenden Objekts tun.quelle
memcpy
von einem Objekt des TypsT
zu einem anderen wechseln , das kein Array vonchar
s ist, würde der dtor des Zielobjekts dann nicht UB verursachen?new
dort in der Zwischenzeit ein neues Objekt. Ich lese, dassmemcpy
das Eingreifen in etwas als "Wiederverwendung des Speichers" gilt, sodass die Lebensdauer dessen endet, was zuvor vorhanden war (und da es keinen dtor-Aufruf gibt, haben Sie UB, wenn Sie von der vom dtor verursachten Nebenwirkung abhängen). Die Lebensdauer eines neuen Objekts beginnt jedoch nicht, und Sie erhalten UB später beim impliziten dtor-Aufruf, es sei denn,T
in der Zwischenzeit wird dort ein Ist erstellt .std
damit Ihr Code die Verwendung ungültiger Iteratoren frühzeitig abfängt, anstatt Speicher oder ähnliches zu überschreiben (eine Art instrumentierter Iterator).memcpy
für dieses Objekt Probleme stromabwärts verursacht. Ist das Grund genug zu sagen, dass das Verhalten vonmemcpy
für solche Objekte undefiniert ist?memcpy
und es anschließend einfach verlieren , sollte das Verhalten genau definiert sein (wenn Sie nicht von den Auswirkungen des dtor abhängig sind), auch wenn Sie dort kein neues Objekt erstellen, da es vorhanden ist Kein impliziter dtor-Aufruf, der UB verursachen würde.C ++ garantiert nicht für alle Typen, dass ihre Objekte zusammenhängende Speicherbytes belegen [intro.object] / 5
In der Tat können Sie über virtuelle Basisklassen nicht zusammenhängende Objekte in Hauptimplementierungen erstellen. Ich habe versucht , um ein Beispiel zu bauen , in dem eine Basisklasse Subobjekt eines Objekts
x
befindet , bevorx
‚s - Startadresse . Betrachten Sie zur Veranschaulichung das folgende Diagramm / die folgende Tabelle, in der die horizontale Achse der Adressraum und die vertikale Achse die Vererbungsstufe ist (Stufe 1 erbt von Stufe 0). Mit gekennzeichnete Felderdm
werden von direkten Datenelementen der Klasse belegt.Dies ist ein übliches Speicherlayout bei Verwendung der Vererbung. Der Speicherort eines Unterobjekts der virtuellen Basisklasse ist jedoch nicht festgelegt, da es von untergeordneten Klassen verschoben werden kann, die auch virtuell von derselben Basisklasse erben. Dies kann dazu führen, dass das Objekt der Ebene 1 (Basisklasse-Unterobjekt) meldet, dass es an Adresse 8 beginnt und 16 Byte groß ist. Wenn wir diese beiden Zahlen naiv addieren, würden wir denken, dass sie den Adressraum belegen [8, 24], obwohl sie tatsächlich [0, 16) belegen.
Wenn wir ein solches Objekt der Ebene 1 erstellen können, können wir es nicht
memcpy
zum Kopieren verwenden:memcpy
würde auf Speicher zugreifen, der nicht zu diesem Objekt gehört (Adressen 16 bis 24). Wird in meiner Demo als Stapel-Puffer-Überlauf vom Adress-Desinfektionsprogramm von clang ++ abgefangen.Wie konstruiere ich ein solches Objekt? Durch die Verwendung mehrerer virtueller Vererbungen habe ich ein Objekt mit dem folgenden Speicherlayout gefunden (virtuelle Tabellenzeiger sind als gekennzeichnet
vp
). Es besteht aus vier Vererbungsebenen:Das oben beschriebene Problem tritt für das Unterobjekt der Basisklasse 1 auf. Die Startadresse ist 32 und 24 Byte groß (vptr, eigene Datenelemente und Datenelemente der Ebene 0).
Hier ist der Code für ein solches Speicherlayout unter clang ++ und g ++ @ coliru:
struct l0 { std::int64_t dummy; }; struct l1 : virtual l0 { std::int64_t dummy; }; struct l2 : virtual l0, virtual l1 { std::int64_t dummy; }; struct l3 : l2, virtual l1 { std::int64_t dummy; };
Wir können einen Stapelpufferüberlauf wie folgt erzeugen:
l3 o; l1& so = o; l1 t; std::memcpy(&t, &so, sizeof(t));
Hier ist eine vollständige Demo, die auch einige Informationen zum Speicherlayout druckt:
#include <cstdint> #include <cstring> #include <iomanip> #include <iostream> #define PRINT_LOCATION() \ std::cout << std::setw(22) << __PRETTY_FUNCTION__ \ << " at offset " << std::setw(2) \ << (reinterpret_cast<char const*>(this) - addr) \ << " ; data is at offset " << std::setw(2) \ << (reinterpret_cast<char const*>(&dummy) - addr) \ << " ; naively to offset " \ << (reinterpret_cast<char const*>(this) - addr + sizeof(*this)) \ << "\n" struct l0 { std::int64_t dummy; void report(char const* addr) { PRINT_LOCATION(); } }; struct l1 : virtual l0 { std::int64_t dummy; void report(char const* addr) { PRINT_LOCATION(); l0::report(addr); } }; struct l2 : virtual l0, virtual l1 { std::int64_t dummy; void report(char const* addr) { PRINT_LOCATION(); l1::report(addr); } }; struct l3 : l2, virtual l1 { std::int64_t dummy; void report(char const* addr) { PRINT_LOCATION(); l2::report(addr); } }; void print_range(void const* b, std::size_t sz) { std::cout << "[" << (void const*)b << ", " << (void*)(reinterpret_cast<char const*>(b) + sz) << ")"; } void my_memcpy(void* dst, void const* src, std::size_t sz) { std::cout << "copying from "; print_range(src, sz); std::cout << " to "; print_range(dst, sz); std::cout << "\n"; } int main() { l3 o{}; o.report(reinterpret_cast<char const*>(&o)); std::cout << "the complete object occupies "; print_range(&o, sizeof(o)); std::cout << "\n"; l1& so = o; l1 t; my_memcpy(&t, &so, sizeof(t)); }
Live-Demo
Beispielausgabe (abgekürzt, um vertikales Scrollen zu vermeiden):
Beachten Sie die beiden hervorgehobenen Endversätze.
quelle
sizeof(T)
Bytes, beginnend mit der Adresse des gesamten Objekts, was mein Punkt war. Sie können ein Objekt eines nicht abstrakten Klassentyps in einem ausreichend großen und ausgerichteten Speicher haben. Dies ist eine starke Anforderung auf der Ebene der Sprachsemantik und des Speicherzugriffs: Der gesamte zugewiesene Speicher ist gleichwertig. Speicher kann wiederverwendet werden.Viele dieser Antworten erwähnen, dass
memcpy
Invarianten in der Klasse gebrochen werden könnten, was später zu undefiniertem Verhalten führen würde (und was in den meisten Fällen Grund genug sein sollte, es nicht zu riskieren), aber das scheint nicht das zu sein, was Sie wirklich fragen.Ein Grund, warum der
memcpy
Aufruf selbst als undefiniertes Verhalten angesehen wird, besteht darin, dem Compiler so viel Raum wie möglich zu geben, um Optimierungen basierend auf der Zielplattform vorzunehmen. Durch das Gespräch mit sich selbst UB sein, wird der Compiler erlaubt seltsam, plattformabhängige Dinge zu tun.Betrachten Sie dieses (sehr ausgeklügelte und hypothetische) Beispiel: Für eine bestimmte Hardwareplattform gibt es möglicherweise verschiedene Arten von Speicher, von denen einige für verschiedene Vorgänge schneller sind als andere. Es kann beispielsweise eine Art speziellen Speicher geben, der besonders schnelle Speicherkopien ermöglicht. Ein Compiler für diese (imaginäre) Plattform darf daher alle
TriviallyCopyable
Typen in diesem speziellen Speicher ablegen und implementierenmemcpy
, um spezielle Hardwareanweisungen zu verwenden, die nur auf diesem Speicher funktionieren.Wenn Sie diese Option
memcpy
für Nicht-TriviallyCopyable
Objekte auf dieser Plattform verwenden, kann es immemcpy
Aufruf selbst zu einem Absturz von UNGÜLTIGEM OPCODE auf niedriger Ebene kommen .Vielleicht nicht das überzeugendste Argument, aber der Punkt ist, dass der Standard es nicht verbietet , was nur durch den
memcpy
Aufruf von UB möglich ist .quelle
malloc
, eine Art vonnew
.memcpy kopiert alle Bytes oder tauscht in Ihrem Fall alle Bytes aus. Ein übereifriger Compiler könnte das "undefinierte Verhalten" als Entschuldigung für alle Arten von Unfug nehmen, aber die meisten Compiler werden das nicht tun. Trotzdem ist es möglich.
Nachdem diese Bytes kopiert wurden, ist das Objekt, in das Sie sie kopiert haben, möglicherweise kein gültiges Objekt mehr. Ein einfacher Fall ist eine Zeichenfolgenimplementierung, bei der große Zeichenfolgen Speicher zuweisen, kleine Zeichenfolgen jedoch nur einen Teil des Zeichenfolgenobjekts verwenden, um Zeichen zu speichern und einen Zeiger darauf zu behalten. Der Zeiger zeigt offensichtlich auf das andere Objekt, sodass die Dinge falsch sind. Ein anderes Beispiel, das ich gesehen habe, war eine Klasse mit Daten, die nur in sehr wenigen Fällen verwendet wurden, sodass Daten in einer Datenbank mit der Adresse des Objekts als Schlüssel gespeichert wurden.
Wenn Ihre Instanzen beispielsweise einen Mutex enthalten, würde ich denken, dass das Verschieben ein großes Problem sein könnte.
quelle
Ein weiterer Grund
memcpy
für UB (abgesehen von dem, was in den anderen Antworten erwähnt wurde - es könnte später zu Invarianten führen) ist, dass es für den Standard sehr schwierig ist, genau zu sagen, was passieren würde .Für nicht triviale Typen sagt der Standard sehr wenig darüber aus, wie das Objekt im Speicher angeordnet ist, in welcher Reihenfolge die Elemente platziert werden, wo sich der vtable-Zeiger befindet, wie das Auffüllen sein soll usw. Der Compiler verfügt über enorme Freiheiten bei der Entscheidung.
Selbst wenn der Standard dies
memcpy
in diesen "sicheren" Situationen zulassen wollte , wäre es daher unmöglich anzugeben, welche Situationen sicher sind und welche nicht oder wann genau die tatsächliche UB für unsichere Fälle ausgelöst würde.Ich nehme an, Sie könnten argumentieren, dass die Auswirkungen implementierungsdefiniert oder nicht spezifiziert sein sollten, aber ich persönlich würde der Meinung sein, dass dies sowohl ein wenig zu tief in die Plattformspezifikationen eingreift als auch etwas, das im allgemeinen Fall ein wenig zu legitimiert ist ist eher unsicher.
quelle
memcpy(buffer, p, sizeof (T))
, wobuffer
einchar[sizeof (T)];
sollte erlaubt sein , etwas anderes als schreiben Sie einige Bytes in den Puffer zu tun?Beachten Sie zunächst, dass es unbestreitbar ist, dass der gesamte Speicher für veränderbare C / C ++ - Objekte nicht typisiert, nicht spezialisiert und für jedes veränderbare Objekt verwendbar sein muss. (Ich denke, der Speicher für globale const-Variablen könnte hypothetisch typisiert werden. Es macht einfach keinen Sinn, eine solche Hyperkomplikation für einen so kleinen Eckfall durchzuführen.) Im Gegensatz zu Java hat C ++ keine typisierte Zuordnung eines dynamischen Objekts :
new Class(args)
In Java handelt es sich um eine typisierte Objekterstellung : Erstellen eines Objekts eines genau definierten Typs, das möglicherweise im typisierten Speicher gespeichert ist. Auf der anderen Seite ist der C ++ - Ausdrucknew Class(args)
nur ein dünner Typisierungs-Wrapper um die typlose Speicherzuweisung, der entsprichtnew (operator new(sizeof(Class)) Class(args)
: Das Objekt wird im "neutralen Speicher" erstellt. Das zu ändern würde bedeuten, einen sehr großen Teil von C ++ zu ändern.Das Verbot der Bitkopieroperation (unabhängig davon, ob sie von einem
memcpy
oder einem äquivalenten benutzerdefinierten byteweisen Kopiervorgang ausgeführt wird) für einen Typ bietet der Implementierung für polymorphe Klassen (solche mit virtuellen Funktionen) und andere sogenannte "virtuelle Klassen" (nicht a) viel Freiheit Standardbegriff), das sind die Klassen, die dasvirtual
Schlüsselwort verwenden.Die Implementierung polymorpher Klassen könnte eine globale assoziative Zuordnungskarte verwenden, die die Adresse eines polymorphen Objekts und seine virtuellen Funktionen verknüpft. Ich glaube, das war eine Option, die beim Entwurf der ersten Iterationen der C ++ - Sprache (oder sogar "C mit Klassen") ernsthaft in Betracht gezogen wurde. Diese Karte polymorpher Objekte verwendet möglicherweise spezielle CPU-Funktionen und speziellen assoziativen Speicher (solche Funktionen sind für den C ++ - Benutzer nicht verfügbar).
Natürlich wissen wir, dass alle praktischen Implementierungen virtueller Funktionen vtables (einen konstanten Datensatz, der alle dynamischen Aspekte einer Klasse beschreibt) verwenden und in jedes polymorphe Basisklassen-Unterobjekt einen vptr (vtable-Zeiger) einfügen, da dieser Ansatz äußerst einfach zu implementieren ist (at am wenigsten für die einfachsten Fälle) und sehr effizient. Es gibt keine globale Registrierung von polymorphen Objekten in einer realen Implementierung, außer möglicherweise im Debug-Modus (ich kenne einen solchen Debug-Modus nicht).
Der C ++ - Standard machte das Fehlen einer globalen Registrierung etwas offiziell, indem er sagte, dass Sie den Destruktoraufruf überspringen können, wenn Sie den Speicher eines Objekts wiederverwenden, solange Sie nicht von den "Nebenwirkungen" dieses Destruktoraufrufs abhängig sind. (Ich glaube, das bedeutet, dass die "Nebenwirkungen" vom Benutzer erstellt wurden, dh der Hauptteil des Destruktors, nicht die erstellte Implementierung, wie dies von der Implementierung automatisch für den Destruktor getan wird.)
In der Praxis verwendet der Compiler in allen Implementierungen nur versteckte vptr-Elemente (Zeiger auf vtables), und diese ausgeblendeten Elemente werden von ordnungsgemäß kopiert
memcpy
;; als ob Sie eine einfache, kopierweise Kopie der C-Struktur erstellt hätten, die die polymorphe Klasse darstellt (mit all ihren versteckten Elementen). Bitweise Kopien oder vollständige Kopien von C-Strukturelementen (die vollständige C-Struktur enthält ausgeblendete Elemente) verhalten sich genau wie ein Konstruktoraufruf (wie durch Platzieren von new ausgeführt). Alles, was Sie tun müssen, lässt den Compiler denken, dass Sie dies könnten habe Platzierung neu genannt. Wenn Sie einen stark externen Funktionsaufruf ausführen (einen Aufruf einer Funktion, die nicht eingebunden werden kann und deren Implementierung vom Compiler nicht geprüft werden kann, wie einen Aufruf einer in einer dynamisch geladenen Codeeinheit definierten Funktion oder einen Systemaufruf), dann ist der Der Compiler geht lediglich davon aus, dass solche Konstruktoren von dem Code aufgerufen wurden, den er nicht untersuchen kann. So ist das Verhalten vonmemcpy
Hier wird nicht durch den Sprachstandard definiert, sondern durch den Compiler ABI (Application Binary Interface). Das Verhalten eines stark externen Funktionsaufrufs wird vom ABI definiert, nicht nur vom Sprachstandard. Ein Aufruf einer potenziell inlinierbaren Funktion wird von der Sprache definiert, da ihre Definition sichtbar ist (entweder während des Compilers oder während der globalen Optimierung der Verbindungszeit).In der Praxis können Sie also bei geeigneten "Compiler-Zäunen" (z. B. beim Aufruf einer externen Funktion oder nur
asm("")
)memcpy
Klassen verwenden, die nur virtuelle Funktionen verwenden.Natürlich muss Ihnen die Sprachsemantik erlauben, eine solche Platzierung neu
memcpy
durchzuführen, wenn Sie Folgendes tun : Sie können den dynamischen Typ eines vorhandenen Objekts nicht ohne weiteres neu definieren und so tun, als hätten Sie das alte Objekt nicht einfach zerstört. Wenn Sie ein nicht konstantes globales, statisches, automatisches Element-Unterobjekt oder Array-Unterobjekt haben, können Sie es überschreiben und ein anderes, nicht verwandtes Objekt dort ablegen. Wenn der dynamische Typ jedoch unterschiedlich ist, können Sie nicht so tun, als wäre es immer noch dasselbe Objekt oder Unterobjekt:struct A { virtual void f(); }; struct B : A { }; void test() { A a; if (sizeof(A) != sizeof(B)) return; new (&a) B; // OK (assuming alignement is OK) a.f(); // undefined }
Die Änderung des polymorphen Typs eines vorhandenen Objekts ist einfach nicht zulässig: Das neue Objekt hat keine Beziehung zu
a
außer dem Speicherbereich: den fortlaufenden Bytes ab&a
. Sie haben verschiedene Arten.[Der Standard ist stark gespalten darüber, ob
*&a
(in typischen Flachspeichermaschinen) oder(A&)(char&)a
(auf jeden Fall) verwendet werden kann, um auf das neue Objekt zu verweisen. Compiler-Autoren sind nicht geteilt: Sie sollten es nicht tun. Dies ist ein tiefer Fehler in C ++, vielleicht der tiefste und beunruhigendste.]In portablem Code können Sie jedoch keine bitweise Kopie von Klassen ausführen, die virtuelle Vererbung verwenden, da einige Implementierungen diese Klassen mit Zeigern auf die virtuellen Basisunterobjekte implementieren: Bei diesen Zeigern, die vom Konstruktor des am meisten abgeleiteten Objekts ordnungsgemäß initialisiert wurden, wird der Wert von kopiert
memcpy
(wie eine einfache mitgliedsweise Kopie der C-Struktur, die die Klasse mit all ihren versteckten Elementen darstellt) und würde nicht auf das Unterobjekt des abgeleiteten Objekts zeigen!Andere ABI verwenden Adressversätze, um diese Basisunterobjekte zu lokalisieren. Sie hängen nur vom Typ des am meisten abgeleiteten Objekts ab, wie z. B. endgültige Überschreibungen und
typeid
, und können daher in der vtable gespeichert werden. Funktioniert bei dieser Implementierungmemcpy
wie vom ABI garantiert (mit der oben genannten Einschränkung beim Ändern des Typs eines vorhandenen Objekts).In beiden Fällen handelt es sich ausschließlich um ein Problem der Objektdarstellung, dh um ein ABI-Problem.
quelle
memcpy
in der Praxis polymorphe Klassen verwenden, sofern der ABI dies impliziert. Dies hängt also von der Implementierung ab. In jedem Fall müssen Sie Compiler-Barrieren verwenden, um zu verbergen, was Sie tun (plausible Verleugnung) UND Sie müssen die Sprachsemantik weiterhin respektieren (kein Versuch, den Typ eines vorhandenen Objekts zu ändern).memcpy
nur für die polymorphen Objekttypen berücksichtigen soll.memcpy
einige Typen zu verbieten , war die Implementierung virtueller Funktionen. Für nicht virtuelle Typen habe ich keine Ahnung!Was ich hier wahrnehmen kann, ist, dass der C ++ - Standard für einige praktische Anwendungen möglicherweise zu restriktiv oder eher nicht zulässig genug ist.
Wie in anderen Antworten gezeigt
memcpy
unten schnell Pausen für „kompliziert“ Typen, aber IMHO, ist es eigentlich sollte für Standard - Layout - Typen arbeiten , solange dasmemcpy
nicht das, was nicht bricht die definierten Kopie-Operationen und destructor des Standardlayout Art tun. (Beachten Sie, dass eine gerade TC-Klasse einen nicht trivialen Konstruktor haben darf .) Der Standard ruft nur explizit TC-Typen wrt auf. dies jedoch.Ein aktueller Zitatentwurf (N3797):
Der Standard spricht hier von trivial kopierbaren Typen, aber wie oben von @dyp beobachtet , gibt es auch Standardlayouttypen , die sich meines Erachtens nicht unbedingt mit trivial kopierbaren Typen überschneiden.
Der Standard sagt:
Was ich hier sehe, ist Folgendes:
memcpy
. (wie hier schon mehrfach erwähnt)memcpy
von Objekten mit Standardlayout, die nicht trivial kopierbar sind.Es scheint also nicht explizit UB genannt zu werden, aber es ist sicherlich auch nicht das, was als nicht spezifiziertes Verhalten bezeichnet wird , so dass man schließen könnte, was @underscore_d im Kommentar zur akzeptierten Antwort getan hat:
Ich persönlich würde zu dem Schluss kommen, dass es sich bei der Portabilität um UB handelt (oh, diese Optimierer), aber ich denke, dass man mit etwas Absicherung und Wissen über die konkrete Implementierung damit durchkommen kann. (Stellen Sie nur sicher, dass es die Mühe wert ist.)
Randnotiz: Ich denke auch, dass der Standard die Semantik vom Typ Standardlayout wirklich explizit in das gesamte
memcpy
Durcheinander einbeziehen sollte , da dies ein gültiger und nützlicher Anwendungsfall ist, um nicht trivial kopierbare Objekte bitweise zu kopieren, aber das ist hier nicht der Punkt.Link: Kann ich memcpy verwenden, um in mehrere benachbarte Standardlayout-Unterobjekte zu schreiben?
quelle
memcpy
Lage ist, solche Objekte über Standardkonstruktoren zum Kopieren / Verschieben und Zuweisen von Operationen zu verfügen, die als einfache byteweise Kopien definiert sindmemcpy
. Wenn ich sage, dass mein Typ in dermemcpy
Lage ist, aber eine nicht standardmäßige Kopie hat, widerspreche ich mir selbst und meinem Vertrag mit dem Compiler, der besagt, dass für TC-Typen nur die Bytes von Bedeutung sind. Auch wenn meine benutzerdefinierte Kopie Ctor / assign gerade tut eine byteweise Kopie & fügt eine Diagnosemeldung,++
sastatic
Zähler oder etwas - dass ich den Compiler meinen Code erwarten impliziert zu analysieren und beweist , dass es nicht mit Zohan an Bytedarstellung.memcpy
der Compiler für jeden Typ unrealistische / unfaire Mengen statischer Analysen durchführen. Ich habe nicht aufgezeichnet, dass dies die Motivation ist, aber es scheint überzeugend. Aber wenn wir cppreference glauben -Standard layout types are useful for communicating with code written in other programming languages
- sind sie viel Gebrauch ohne die Sprachen der Lage, Kopien in definierter Weise zu nehmen? Ich denke, wir können dann nur dann einen Zeiger ausgeben, wenn wir ihn sicher auf C ++ - Seite zugewiesen haben.Ok, versuchen wir Ihren Code anhand eines kleinen Beispiels:
#include <iostream> #include <string> #include <string.h> void swapMemory(std::string* ePtr1, std::string* ePtr2) { static const int size = sizeof(*ePtr1); char swapBuffer[size]; memcpy(swapBuffer, ePtr1, size); memcpy(ePtr1, ePtr2, size); memcpy(ePtr2, swapBuffer, size); } int main() { std::string foo = "foo", bar = "bar"; std::cout << "foo = " << foo << ", bar = " << bar << std::endl; swapMemory(&foo, &bar); std::cout << "foo = " << foo << ", bar = " << bar << std::endl; return 0; }
Auf meinem Computer wird vor dem Absturz Folgendes gedruckt:
Seltsam, was? Der Tausch scheint überhaupt nicht durchgeführt zu werden. Nun, der Speicher wurde ausgetauscht, verwendet aber
std::string
die Small-String-Optimierung auf meinem Computer: Er speichert kurze Strings in einem Puffer, der Teil desstd::string
Objekts selbst ist, und zeigt nur mit seinem internen Datenzeiger auf diesen Puffer.Wenn
swapMemory()
die Bytes ausgetauscht werden, werden sowohl die Zeiger als auch die Puffer ausgetauscht. Der Zeiger imfoo
Objekt zeigt nun auf den Speicher imbar
Objekt, der jetzt die Zeichenfolge enthält"foo"
. Zwei Swap-Ebenen machen keinen Swap.Wenn
std::string
der Destruktor anschließend versucht, aufzuräumen, passiert mehr Böses: Der Datenzeiger zeigt nicht mehr auf denstd::string
internen Puffer des eigenen, sodass der Destruktor daraus schließt, dass dieser Speicher auf dem Heap zugewiesen worden sein muss, und versuchtdelete
es. Das Ergebnis auf meinem Computer ist ein einfacher Absturz des Programms, aber dem C ++ - Standard wäre es egal, ob rosa Elefanten auftauchen würden. Das Verhalten ist völlig undefiniert.Und das ist der grundlegende Grund, warum Sie nicht für
memcpy()
nicht trivial kopierbare Objekte verwenden sollten: Sie wissen nicht, ob das Objekt Zeiger / Verweise auf seine eigenen Datenelemente enthält oder auf andere Weise von seinem eigenen Speicherort im Speicher abhängt. Wenn Sie einmemcpy()
solches Objekt verwenden, wird die Grundannahme verletzt, dass sich das Objekt nicht im Speicher bewegen kann, und einige Klassen wiestd::string
diese stützen sich auf diese Annahme. Der C ++ - Standard zeichnet die Grenze zwischen (nicht) trivial kopierbaren Objekten, um zu vermeiden, dass mehr auf unnötige Details zu Zeigern und Referenzen eingegangen wird. Es macht nur eine Ausnahme für trivial kopierbare Objekte und sagt: Nun, in diesem Fall sind Sie sicher. Aber beschuldigen Sie mich nicht für die Konsequenzen, wenn Sie versuchen,memcpy()
andere Objekte zu verwenden.quelle