ein trivial kopierbares Objekt mit memcpy “konstruieren”.

Lesezeit: 13 Minuten

ein trivial kopierbares Objekt mit memcpy konstruieren
MM

Ist dieser Code in C++ korrekt?

#include <cstdlib>
#include <cstring>

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    std::memcpy(buf, &a, sizeof a);
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

Mit anderen Worten, ist *b ein Objekt, dessen Lebenszeit begonnen hat? (Wenn ja, wann genau hat es angefangen?)

  • Siehe auch: stackoverflow.com/questions/26171827/…

    – MM

    8. Mai 2015 um 1:36 Uhr

  • Das einzige mögliche Problem, das mir einfällt, ist striktes Aliasing. Was Sie korrigieren könnten, indem Sie den Typ von ändern buf In diesem Fall würde ich sagen, dass beides b und buff ein und dasselbe sind, also dieselbe Lebensdauer haben.

    – Unsinn

    8. Mai 2015 um 1:44 Uhr

  • @nonsensickle Ich glaube nicht, dass hier strenges Aliasing gilt: if *b ist ein Objekt vom Typ T dann gibt es keine Aliasing-Verletzung, um es als solches zu verwenden; und wenn nicht, dann ist es UB, weil b->y versucht ein nicht existierendes Objekt zu lesen . Sicherlich würde es keinen Unterschied machen, die Art von zu ändern buf; Das Casting eines Zeigers ändert nicht den dynamischen Typ des Objekts, auf das er zeigt

    – MM

    8. Mai 2015 um 1:54 Uhr

  • Ja, ich denke du hast recht. Solange Sie nicht verwenden buf als ein Wert es sollte nicht gegen die strikte Aliasing-Regel verstoßen. Ich ziehe mein Argument zurück, aber ich hinterlasse den Kommentar, wenn es Ihnen nichts ausmacht.

    – Unsinn

    8. Mai 2015 um 2:13 Uhr

ein trivial kopierbares Objekt mit memcpy konstruieren
Shafik Yaghmur

Dies ist nicht spezifiziert, was von unterstützt wird N3751: Objektlebensdauer, Low-Level-Programmierung und memcpy der unter anderem sagt:

Die C++-Standards schweigen derzeit darüber, ob die Verwendung von memcpy zum Kopieren von Objektdarstellungsbytes konzeptionell eine Zuweisung oder eine Objektkonstruktion ist. Der Unterschied spielt für semantikbasierte Programmanalyse- und Transformationstools sowie für Optimierer, die die Objektlebensdauer verfolgen, eine Rolle. Darauf deutet dieses Papier hin

  1. Die Verwendung von memcpy zum Kopieren der Bytes zweier unterschiedlicher Objekte zweier verschiedener trivialer kopierbarer Tabellen (aber ansonsten gleicher Größe) ist zulässig

  2. solche Verwendungen werden als Initialisierung oder allgemeiner als (konzeptionell) Objektkonstruktion anerkannt.

Die Erkennung als Objektkonstruktion wird binäre IO unterstützen, während sie weiterhin lebensdauerbasierte Analysen und Optimierer zulässt.

Ich kann kein Sitzungsprotokoll finden, in dem dieses Papier diskutiert wurde, also scheint es, als wäre es immer noch ein offenes Thema.

Der C++14-Standardentwurf sagt derzeit in 1.8 [intro.object]:

[…]Ein Objekt wird bei Bedarf durch eine Definition (3.1), durch einen Neu-Ausdruck (5.3.4) oder durch die Implementierung (12.2) erzeugt.[…]

was wir mit dem nicht haben malloc und die im Standard behandelten Fälle zum Kopieren trivialer kopierbarer Typen scheinen sich nur auf bereits vorhandene Objekte im Abschnitt zu beziehen 3.9 [basic.types]:

Für jedes Objekt (außer einem Unterobjekt der Basisklasse) des trivial kopierbaren Typs T können die zugrunde liegenden Bytes (1.7), aus denen das Objekt besteht, in ein Array von char oder kopiert werden, unabhängig davon, ob das Objekt einen gültigen Wert vom Typ T enthält oder nicht unsigned char.42 Wenn der Inhalt des Arrays von char oder unsigned char zurück in das Objekt kopiert wird, soll das Objekt anschließend seinen ursprünglichen Wert behalten[…]

und:

Wenn für jeden trivial kopierbaren Typ T zwei Zeiger auf T auf unterschiedliche T-Objekte obj1 und obj2 zeigen, wobei weder obj1 noch obj2 ein Unterobjekt der Basisklasse sind, wenn die zugrunde liegenden Bytes (1.7), aus denen obj1 besteht, in obj2,43 obj2 kopiert werden soll anschließend den gleichen Wert wie obj1 haben.[…]

das ist im Grunde das, was der Vorschlag sagt, also sollte das nicht überraschen.

dyp weist auf eine spannende Diskussion zu diesem Thema hin ub-Mailingliste: [ub] Geben Sie Wortspiele ein, um ein Kopieren zu vermeiden.

Propoal p0593: Implizite Erstellung von Objekten für Objektmanipulation auf niedriger Ebene

Der Vorschlag p0593 versucht, dieses Problem zu lösen, aber AFAIK wurde noch nicht überprüft.

Dieses Papier schlägt vor, dass Objekte ausreichend trivialer Typen nach Bedarf innerhalb neu zugewiesenen Speichers erstellt werden, um Programmen ein definiertes Verhalten zu geben.

Es hat einige motivierende Beispiele, die ähnlicher Natur sind, einschließlich einer Strömung std::Vektor Implementierung, die derzeit undefiniertes Verhalten aufweist.

Es schlägt die folgenden Möglichkeiten vor, um ein Objekt implizit zu erstellen:

Wir schlagen vor, dass mindestens die folgenden Operationen als Objekte implizit erzeugend spezifiziert werden:

  • Die Erstellung eines Arrays aus char, unsigned char oder std::byte erstellt implizit Objekte innerhalb dieses Arrays.

  • Ein Aufruf von malloc, calloc, realloc oder einer beliebigen Funktion namens operator new oder operator new[] erstellt implizit Objekte in seinem zurückgegebenen Speicher.

  • std::allocator::allocate erstellt ebenfalls implizit Objekte in seinem zurückgegebenen Speicher; die Allokator-Anforderungen sollten andere Allokator-Implementierungen erfordern, dasselbe zu tun.

  • Ein Aufruf von memmove verhält sich so, als ob es

    • kopiert den Quellspeicher in einen temporären Bereich

    • erstellt implizit Objekte im Zielspeicher und dann

    • kopiert den temporären Speicher in den Zielspeicher.

    Dadurch kann memmove die Typen trivial kopierbarer Objekte beibehalten oder verwendet werden, um eine Byte-Darstellung eines Objekts als die eines anderen Objekts neu zu interpretieren.

  • Ein Aufruf von memcpy verhält sich genauso wie ein Aufruf von memmove, außer dass er eine Überlappungsbeschränkung zwischen Quelle und Ziel einführt.

  • Ein Klassenmitgliedszugriff, der ein Vereinigungsmitglied nominiert, löst eine implizite Objekterstellung innerhalb des von dem Vereinigungsmitglied belegten Speichers aus. Beachten Sie, dass dies keine völlig neue Regel ist: Diese Berechtigung existierte bereits in [P0137R1] für Fälle, in denen sich der Mitgliedszugriff auf der linken Seite einer Zuweisung befindet, aber jetzt als Teil dieses neuen Frameworks verallgemeinert wird. Wie unten erläutert, erlaubt dies kein Typ-Wortspiel durch Vereinigungen; vielmehr lässt es lediglich zu, dass das aktive Union-Member durch einen Klassenmember-Zugriffsausdruck geändert wird.

  • Eine neue Barrier-Operation (anders als std::launder, die keine Objekte erstellt) sollte in die Standardbibliothek eingeführt werden, mit einer Semantik, die einem memmove mit demselben Quell- und Zielspeicher entspricht. Als Strohmann schlagen wir vor:

    // Erfordert: [start, (char*)start + length) denotes a region of allocated
    // storage that is a subset of the region of storage reachable through start.
    // Effects: implicitly creates objects within the denoted region.
    void std::bless(void *start, size_t length);
    

In addition to the above, an implementation-defined set of non-stasndard memory allocation and mapping functions, such as mmap on POSIX systems and VirtualAlloc on Windows systems, should be specified as implicitly creating objects.

Note that a pointer reinterpret_cast is not considered sufficient to trigger implicit object creation.

  • @dyp wow, that is an awesome discussion, it is going to take a while to digest it but it is priceless, Thank you for pointing that out.

    – Shafik Yaghmour

    May 10, 2015 at 17:03

  • Unfortunately, it is incomplete as far as I can tell (the beginning is missing and the conclusion is vague at best IMHO).

    – dyp

    May 10, 2015 at 18:01

  • I think you meant “not specified” rather than “unspecified” (the latter term has a specific meaning in the C++ standard) ?

    – M.M

    Jun 10, 2015 at 23:46

  • Also I have a corollary question (not sure if it is worth posting this as a separate question or not); do you feel it would make any difference if T had a non-trivial default constructor? (But is still trivially-copyable).

    – M.M

    Jun 10, 2015 at 23:57

  • On the other hand, the “does memcpy create an object” question seems more motivated by general purpose manipulation of trivially copyable types. For example, it seems “obvious” that when when std::vector needs to expand and copy it’s underlying storage consisting of trivially copyable T objects, it can simply allocated new uninitialized storage of a larger size, and memcpy the existing over objects (indeed the standard explicitly guarantees that such copies between two T objects is well-defined). It’s not allowed though because there is noT object yet in the uninitialized storage.

    – BeeOnRope

    Oct 22, 2017 at 20:50

The code is legal now, and retroactively since C++98!

The answer by @Shafik Yaghmour is thorough and relates to the code validity as an open issue – which was the case when answered. Shafik’s answer correctly refer to p0593 which at the time of the answer was a proposal. But since then, the proposal was accepted and things got defined.

Some History

The possibility of creating an object using malloc was not mentioned in the C++ specification before C++20, see for example C++17 spec [intro.object]:

Die Konstrukte in einem C++-Programm erstellen, zerstören, beziehen sich auf, greifen auf Objekte zu und manipulieren sie. Ein Objekt wird durch eine Definition (6.1), durch einen neuen Ausdruck (8.5.2.4), beim impliziten Ändern des aktiven Mitglieds einer Union (12.3) oder beim Erstellen eines temporären Objekts (7.4, 15.2) erstellt.

Der obige Wortlaut bezieht sich nicht auf malloc als Option zum Erstellen eines Objekts, wodurch es zu einem wird de facto undefiniertes Verhalten.

Es war dann als Problem angesehenund dieses Problem wurde später von behoben https://wg21.link/P0593R6 und als DR gegen alle C++-Versionen seit C++98 einschließlich akzeptiert und dann in die C++20-Spezifikation mit dem neuen Wortlaut aufgenommen:

[intro.object]

  1. Die Konstrukte in einem C++-Programm erstellen, zerstören, beziehen sich auf, greifen auf Objekte zu und manipulieren sie. Ein Objekt wird durch eine Definition, durch einen New-Ausdruck, durch eine Operation, die implizit Objekte erzeugt (siehe unten)

  1. Ferner werden nach dem impliziten Erstellen von Objekten innerhalb eines spezifizierten Speicherbereichs einige Operationen so beschrieben, dass sie einen Zeiger auf ein geeignetes erstelltes Objekt erzeugen. Diese Operationen wählen eines der implizit erzeugten Objekte aus, dessen Adresse die Adresse des Anfangs des Speicherbereichs ist, und erzeugen einen Zeigerwert, der auf dieses Objekt zeigt, wenn dieser Wert dazu führen würde, dass das Programm ein definiertes Verhalten aufweist. Wenn kein solcher Zeigerwert dem Programm definiertes Verhalten geben würde, ist das Verhalten des Programms undefiniert. Wenn mehrere solcher Zeigerwerte dem Programm ein definiertes Verhalten geben würden, ist nicht spezifiziert, welcher solcher Zeigerwert erzeugt wird.

Die Beispiel In der C++20-Spezifikation angegeben ist:

#include <cstdlib>
struct X { int a, b; };
X *make_x() {
   // The call to std​::​malloc implicitly creates an object of type X
   // and its subobjects a and b, and returns a pointer to that X object
   // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
   // in order to give the subsequent class member access operations   
   // defined behavior. 
   X *p = (X*)std::malloc(sizeof(struct X));
   p->a = 1;   
   p->b = 2;
   return p;
}

Was die Verwendung von memcpy – @Shafik Yaghmour spricht das bereits an, dieser Teil gilt für trivial kopierbare Typen (der Wortlaut geändert von POD in C++98 und C++03 zu trivial kopierbare Typen in C++11 und danach).


Endeffekt: der Code ist gültig.

Was die Frage der Lebensdauer betrifft, lassen Sie uns in den fraglichen Code eintauchen:

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) ); // <= just an allocation
    if ( !buf ) return 0;

    T a{}; // <= here an object is born of course
    std::memcpy(buf, &a, sizeof a);      // <= just a copy of bytes
    T *b = static_cast<T *>(buf);        // <= here an object is "born"
                                         //    without constructor    
    b->x = b->y;

    free(buf);
} 

Beachten Sie, dass man dem Destruktor von einen Aufruf hinzufügen kann *bder Vollständigkeit halber, vor dem Freigeben buf:

b->~T();
free(buf);

obwohl dies wird von der Spezifikation nicht verlangt.

Alternative, Löschen b ist auch eine möglichkeit:

delete b;
// instead of:
// free(buf);

Aber wie gesagt, der Code ist so gültig wie er ist.

Von eine schnelle Suche.

“… die Lebensdauer beginnt, wenn der ordnungsgemäß ausgerichtete Speicher für das Objekt zugewiesen wird, und endet, wenn die Speicherzuweisung aufgehoben oder von einem anderen Objekt wiederverwendet wird.”

Also, ich würde nach dieser Definition sagen, die Lebensdauer beginnt mit der Zuteilung und endet mit dem Gratis.

  • Es scheint ein bisschen faul, das zu sagen void *buf = malloc( sizeof(T) ) hat ein Objekt vom Typ erstellt T. Schließlich hätte es genauso gut ein Objekt beliebiger Art erstellen können, dessen Größe gleich ist sizeof(T) wir wissen noch nicht, ob dieser Code weitergehen wird T *b dabei, bzw U *u zum Beispiel

    – MM

    8. Mai 2015 um 2:02 Uhr

  • @nonsensickle Ich hoffe auf eine Antwort in “Sprachrechtsanwaltsqualität”, z. B. Text aus dem C ++ – Standard, um zu unterstützen, dass malloc als trivialer Konstruktor angesehen werden kann

    – MM

    8. Mai 2015 um 2:25 Uhr

  • @MattMcNabb, Erinnerung von malloc hat keine deklarierter Typ“. stackoverflow.com/questions/31483064/… Als solches ist es effektiver Typ kann sich während seiner Lebensdauer viele Male ändern; Jedes Mal, wenn es geschrieben wird, nimmt es den Typ der geschriebenen Daten an. Insbesondere beantwortet das Zitate wie memcpy kopiert den effektiven Typ der Quelldaten. Aber ich denke, das ist C, nicht C++, und vielleicht ist es anders

    – Aaron McDaid

    21. Juli 2015 um 17:22 Uhr

  • @curiousguy: Die strenge Aliasing-Regel wäre ohne das Konzept des “effektiven Typs” bedeutungslos. Andererseits halte ich das Konzept der typbasierten Aliasing-Regeln selbst für einen Fehler, da es Programmierer gleichzeitig dazu zwingt, ineffizienten Code zu schreiben memcpy oder memmove und hoffen, dass ein Optimierer das Problem beheben kann, während er es Compilern versäumt, einfache und einfache Optimierungen vorzunehmen, wenn ein Programmierer weiß (und dem Compiler sagen könnte), dass bestimmte Dinge keinen Alias ​​erhalten.

    – Superkatze

    15. September 2015 um 19:05 Uhr

  • @curiousguy: Ich dachte, es wäre so (was der Grund war char Sonderbehandlung erhalten)? Obwohl ich zugeben muss, dass ich nicht alle Regeln verstehe, was legitim ist und was nicht, da die Regeln schrecklich sind im Vergleich zu dem, was durch das Hinzufügen von a erreicht werden könnte __cache(x) {block} Anweisung, die einen Compiler dazu berechtigen würde anzunehmen, dass der Wert von x wird auf keinen Fall außerhalb der Kontrolle des angehängten Blocks geändert. Jeder Compiler könnte mit einer solchen Anweisung kompatibel sein, indem er sie einfach hat __cache(x) ein Makro sein, das zu nichts expandiert, aber es würde Compilern erlauben, viel zu registrieren …

    – Superkatze

    15. September 2015 um 21:05 Uhr

1646247618 173 ein trivial kopierbares Objekt mit memcpy konstruieren
Rémy Lebeau

Ist dieser Code korrekt?

Nun, es wird normalerweise “funktionieren”, aber nur für triviale Typen.

Ich weiß, dass Sie nicht danach gefragt haben, aber lassen Sie uns ein Beispiel mit einem nicht trivialen Typ verwenden:

#include <cstdlib>
#include <cstring>
#include <string>

struct T   // trivially copyable type
{
    std::string x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T a{};
    a.x = "test";

    std::memcpy(buf, &a, sizeof a);    
    T *b = static_cast<T *>(buf);

    b->x = b->y;

    free(buf);
}

Nach dem Bau a, a.x wird ein Wert zugeordnet. Nehmen wir das an std::string ist nicht für die Verwendung eines lokalen Puffers für kleine Zeichenfolgenwerte optimiert, sondern nur für einen Datenzeiger auf einen externen Speicherblock. Die memcpy() kopiert die internen Daten von a wie es ist buf. Jetzt a.x und b->x beziehen sich auf die gleiche Speicheradresse für die string Daten. Wann b->x ein neuer Wert zugewiesen wird, wird dieser Speicherblock freigegeben, aber a.x verweist immer noch darauf. Wann a geht dann am Ende aus dem Geltungsbereich main(), wird versucht, denselben Speicherblock erneut freizugeben. Undefiniertes Verhalten tritt auf.

Wenn Sie “richtig” sein wollen, ist der richtige Weg, ein Objekt in einen vorhandenen Speicherblock zu konstruieren, die Verwendung von Platzierung-neu Operator statt, zB:

#include <cstdlib>
#include <cstring>

struct T   // does not have to be trivially copyable
{
    // any members
};

int main()
{
    void *buf = std::malloc( sizeof(T) );
    if ( !buf ) return 0;

    T *b = new(buf) T; // <- placement-new
    // calls the T() constructor, which in turn calls
    // all member constructors...

    // b is a valid self-contained object,
    // use as needed...

    b->~T(); // <-- no placement-delete, must call the destructor explicitly
    free(buf);
}

915150cookie-checkein trivial kopierbares Objekt mit memcpy “konstruieren”.

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy