Variation des Typ-Punning-Themas: In-Place-Trivial-Konstruktion

9

Ich weiß, dass dies ein ziemlich häufiges Thema ist, aber so leicht die typische UB zu finden ist, habe ich diese Variante bisher nicht gefunden.

Daher versuche ich, Pixelobjekte formell einzuführen und dabei eine tatsächliche Kopie der Daten zu vermeiden.

Ist das gültig?

struct Pixel {
    uint8_t red;
    uint8_t green;
    uint8_t blue;
    uint8_t alpha;
};

static_assert(std::is_trivial_v<Pixel>);

Pixel* promote(std::byte* data, std::size_t count)
{
    Pixel * const result = reinterpret_cast<Pixel*>(data);
    while (count-- > 0) {
        new (data) Pixel{
            std::to_integer<uint8_t>(data[0]),
            std::to_integer<uint8_t>(data[1]),
            std::to_integer<uint8_t>(data[2]),
            std::to_integer<uint8_t>(data[3])
        };
        data += sizeof(Pixel);
    }
    return result; // throw in a std::launder? I believe it is not mandatory here.
}

Erwartetes Verwendungsmuster, stark vereinfacht:

std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data

Genauer:

  • Hat dieser Code ein genau definiertes Verhalten?
  • Wenn ja, ist es sicher, den zurückgegebenen Zeiger zu verwenden?
  • Wenn ja, auf welche anderen PixelTypen kann es erweitert werden? (Lockerung der is_trivial-Einschränkung? Pixel mit nur 3 Komponenten?).

Sowohl clang als auch gcc optimieren die gesamte Schleife ins Nichts, was ich will. Jetzt möchte ich wissen, ob dies gegen einige C ++ - Regeln verstößt oder nicht.

Godbolt-Link, wenn Sie damit herumspielen möchten.

(Hinweis: Ich habe c ++ 17 trotz nicht markiert std::byte, da die Frage mit verwendet wird char)

Spektren
quelle
2
Aber zusammenhängendes Pixels, das neu platziert wird, ist immer noch kein Array von Pixels.
Jarod42
1
@spectras Das macht aber kein Array. Sie haben nur ein paar Pixelobjekte nebeneinander. Das unterscheidet sich von einem Array.
NathanOliver
1
Also nein wo machst du pixels[some_index]oder *(pixels + something)? Das wäre UB.
NathanOliver
1
Der relevante Abschnitt ist hier und die Schlüsselphrase ist, wenn P auf ein Array-Element i eines Array-Objekts x zeigt . Hier ist pixels(P) kein Zeiger auf ein Array-Objekt, sondern ein Zeiger auf ein einzelnes Pixel. Das heißt, Sie können nur pixels[0]legal zugreifen .
NathanOliver
3
Sie möchten wg21.link/P0593 lesen .
ecatmur

Antworten:

3

Es ist ein undefiniertes Verhalten, das Ergebnis von promoteals Array zu verwenden. Wenn wir uns [expr.add] /4.2 ansehen , haben wir

WennPix andernfalls auf ein Array-Element eines Array-Objekts mit nElementen ([dcl.array]) verweist, zeigen die Ausdrücke P + Jund J + P(wobei Jder Wert j) auf das (möglicherweise hypothetische) Array-Element i+jvon xif 0≤i+j≤nund der Ausdruck P - Jauf das ( möglicherweise hypothetisches Array-Element i−jvon xif 0≤i−j≤n.

Wir sehen, dass der Zeiger tatsächlich auf ein Array-Objekt zeigen muss. Sie haben jedoch kein Array-Objekt. Sie haben einen Zeiger auf eine einzelne Pixel, der zufällig andere Pixelsim zusammenhängenden Speicher folgen. Das heißt, das einzige Element, auf das Sie tatsächlich zugreifen können, ist das erste Element. Der Versuch, auf etwas anderes zuzugreifen, wäre ein undefiniertes Verhalten, da Sie das Ende der gültigen Domäne für den Zeiger überschritten haben.

NathanOliver
quelle
Vielen Dank, dass Sie das so schnell herausgefunden haben. Ich werde stattdessen einen Iterator machen, denke ich. Als Nebenbemerkung bedeutet dies auch, dass &somevector[0] + 1es sich um UB handelt (nun, ich meine, die Verwendung des resultierenden Zeigers wäre).
Spektren
@spectras Das ist eigentlich okay. Sie können den Zeiger immer hinter einem Objekt abrufen. Sie können diesen Zeiger einfach nicht dereferenzieren, selbst wenn dort ein gültiges Objekt vorhanden ist.
NathanOliver
Ja, ich habe den Kommentar bearbeitet, um mich klarer zu machen. Ich wollte den resultierenden Zeiger dereferenzieren :) Vielen Dank für die Bestätigung.
Spektren
@spectras Kein Problem. Dieser Teil von C ++ kann sehr schwierig sein. Auch wenn die Hardware das tut, was wir wollen, ist das eigentlich keine Codierung. Wir codieren auf die abstrakte C ++ - Maschine und es ist eine Persnickety-Maschine;) Hoffentlich wird P0593 übernommen und dies wird viel einfacher.
NathanOliver
1
@spectras Nein, da ein Standardvektor so definiert ist, dass er ein Array enthält, und Sie Zeigerarithmetik zwischen Arrayelementen durchführen können. Es gibt leider keine Möglichkeit, den Standardvektor in C ++ selbst zu implementieren, ohne auf UB zu stoßen.
Yakk - Adam Nevraumont
1

Sie haben bereits eine Antwort bezüglich der eingeschränkten Verwendung des zurückgegebenen Zeigers, aber ich möchte hinzufügen, dass Sie meiner Meinung std::laundernach auch auf den ersten zugreifen müssen Pixel:

Das reinterpret_castgeschieht , bevor ein PixelObjekt erstellt wird (vorausgesetzt , Sie nicht tun so in getSomeImageData). Daher reinterpret_castwird der Zeigerwert nicht geändert. Der resultierende Zeiger zeigt weiterhin auf das erste Element des std::byteArrays, das an die Funktion übergeben wurde.

Wenn Sie die PixelObjekte erstellen , werden sie im Array verschachteltstd::byte und das std::byteArray stellt Speicher für die PixelObjekte bereit.

Es gibt Fälle, in denen die Wiederverwendung von Speicher dazu führt, dass ein Zeiger auf das alte Objekt automatisch auf das neue Objekt zeigt. Aber das ist nicht das, was hier passiert, also resultwird immer noch auf das std::byteObjekt hingewiesen, nicht auf das PixelObjekt. Ich denke, es so zu verwenden, als würde es auf ein PixelObjekt zeigen, wird technisch gesehen ein undefiniertes Verhalten sein.

Ich denke, dass dies immer noch gilt, selbst wenn Sie dies reinterpret_castnach dem Erstellen des PixelObjekts tun , da das PixelObjekt und das std::byte, das Speicher dafür bereitstellt, nicht zeigerinterkonvertierbar sind . Selbst dann würde der Zeiger weiter auf std::bytedas PixelObjekt zeigen , nicht auf das Objekt.

Wenn Sie den Zeiger erhalten haben, um aus dem Ergebnis einer der neuen Platzierungen zurückzukehren, sollte alles in Ordnung sein, was den Zugriff auf dieses bestimmte PixelObjekt betrifft.


Außerdem müssen Sie sicherstellen, dass der std::byteZeiger entsprechend ausgerichtet ist Pixelund dass das Array wirklich groß genug ist. Soweit ich mich erinnere, erfordert der Standard nicht wirklich, dass er Pixeldie gleiche Ausrichtung hat std::byteoder keine Polsterung hat.


Auch hängt nichts davon davon ab Pixel, trivial zu sein oder wirklich irgendeine andere Eigenschaft davon. Alles würde sich gleich verhalten, solange das std::byteArray ausreichend groß und für die PixelObjekte geeignet ausgerichtet ist .

Nussbaum
quelle
Ich glaube das ist richtig. Auch wenn das Array Sache (unimplementability von std::vector) war kein Problem, dann würden Sie noch brauchen , std::launderdas Ergebnis vor eine der placement- Zugriff newed Pixels. Ab sofort ist std::launderhier UB, da die benachbarten Pixels vom gewaschenen Zeiger aus erreichbar wären .
Fureeish
@Fureeish Ich bin mir nicht sicher, warum std::launderUB sein sollte, wenn es resultvor der Rückkehr angewendet wird . Das nebenstehende Pixelist nach meinem Verständnis von eel.is/c++draft/ptr.launder#4 nicht über den gewaschenen Zeiger " erreichbar " . Und selbst ich sehe nicht, wie es UB ist, weil das gesamte ursprüngliche Array vom ursprünglichen Zeiger aus erreichbar ist . std::byte
Walnuss
Der nächste ist Pixeljedoch nicht über den std::byteZeiger erreichbar , sondern über den laundered-Zeiger. Ich glaube, das ist hier relevant. Ich bin jedoch froh, korrigiert zu werden.
Fureeish
@Fureeish Soweit ich weiß, trifft hier keines der angegebenen Beispiele zu, und die Definition der Anforderung sagt auch dasselbe wie der Standard. Die Erreichbarkeit wird in Form von Speicherbytes definiert, nicht in Form von Objekten. Das vom nächsten belegte Byte Pixelscheint mir vom ursprünglichen Zeiger aus erreichbar zu sein, da der ursprüngliche Zeiger auf ein Element des std::byteArrays zeigt, das die Bytes enthält, aus denen der Speicher für das PixelErstellen des " oder innerhalb des unmittelbar umschließenden Arrays besteht, von dem Z ein ist Element "Bedingung gelten (wo Zist Y, dh das std::byteElement selbst).
Walnuss
Ich denke, dass die Speicherbytes, die der nächste Pixelbelegt, nicht über den gewaschenen Zeiger erreichbar sind, da das PixelObjekt , auf das verwiesen wird, kein Element eines Array-Objekts ist und auch nicht mit einem anderen relevanten Objekt in einen Zeiger interkonvertierbar ist. Aber ich denke auch std::launderzum ersten Mal in dieser Tiefe über dieses Detail nach. Auch da bin ich mir nicht 100% sicher.
Walnuss