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 ++ - Ausdruck new Class(args)
nur ein dünner Typisierungs-Wrapper um die typlose Speicherzuweisung, der entspricht new (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 das virtual
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äß kopiertmemcpy
;; 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;
a.f();
}
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 Implementierung memcpy
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.