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
Pixel
Typen 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
)
Pixel
s, das neu platziert wird, ist immer noch kein Array vonPixel
s.pixels[some_index]
oder*(pixels + something)
? Das wäre UB.pixels
(P) kein Zeiger auf ein Array-Objekt, sondern ein Zeiger auf ein einzelnesPixel
. Das heißt, Sie können nurpixels[0]
legal zugreifen .Antworten:
Es ist ein undefiniertes Verhalten, das Ergebnis von
promote
als Array zu verwenden. Wenn wir uns [expr.add] /4.2 ansehen , haben wirWir 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 anderePixels
im 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.quelle
&somevector[0] + 1
es sich um UB handelt (nun, ich meine, die Verwendung des resultierenden Zeigers wäre).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::launder
nach auch auf den ersten zugreifen müssenPixel
:Das
reinterpret_cast
geschieht , bevor einPixel
Objekt erstellt wird (vorausgesetzt , Sie nicht tun so ingetSomeImageData
). Daherreinterpret_cast
wird der Zeigerwert nicht geändert. Der resultierende Zeiger zeigt weiterhin auf das erste Element desstd::byte
Arrays, das an die Funktion übergeben wurde.Wenn Sie die
Pixel
Objekte erstellen , werden sie im Array verschachteltstd::byte
und dasstd::byte
Array stellt Speicher für diePixel
Objekte 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
result
wird immer noch auf dasstd::byte
Objekt hingewiesen, nicht auf dasPixel
Objekt. Ich denke, es so zu verwenden, als würde es auf einPixel
Objekt zeigen, wird technisch gesehen ein undefiniertes Verhalten sein.Ich denke, dass dies immer noch gilt, selbst wenn Sie dies
reinterpret_cast
nach dem Erstellen desPixel
Objekts tun , da dasPixel
Objekt und dasstd::byte
, das Speicher dafür bereitstellt, nicht zeigerinterkonvertierbar sind . Selbst dann würde der Zeiger weiter aufstd::byte
dasPixel
Objekt 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
Pixel
Objekt betrifft.Außerdem müssen Sie sicherstellen, dass der
std::byte
Zeiger entsprechend ausgerichtet istPixel
und dass das Array wirklich groß genug ist. Soweit ich mich erinnere, erfordert der Standard nicht wirklich, dass erPixel
die gleiche Ausrichtung hatstd::byte
oder 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 dasstd::byte
Array ausreichend groß und für diePixel
Objekte geeignet ausgerichtet ist .quelle
std::vector
) war kein Problem, dann würden Sie noch brauchen ,std::launder
das Ergebnis vor eine der placement- Zugriffnew
edPixel
s. Ab sofort iststd::launder
hier UB, da die benachbartenPixel
s vom gewaschenen Zeiger aus erreichbar wären .std::launder
UB sein sollte, wenn esresult
vor der Rückkehr angewendet wird . Das nebenstehendePixel
ist 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
Pixel
jedoch nicht über denstd::byte
Zeiger erreichbar , sondern über denlaunder
ed-Zeiger. Ich glaube, das ist hier relevant. Ich bin jedoch froh, korrigiert zu werden.Pixel
scheint mir vom ursprünglichen Zeiger aus erreichbar zu sein, da der ursprüngliche Zeiger auf ein Element desstd::byte
Arrays zeigt, das die Bytes enthält, aus denen der Speicher für dasPixel
Erstellen des " oder innerhalb des unmittelbar umschließenden Arrays besteht, von dem Z ein ist Element "Bedingung gelten (woZ
istY
, dh dasstd::byte
Element selbst).Pixel
belegt, nicht über den gewaschenen Zeiger erreichbar sind, da dasPixel
Objekt , 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 auchstd::launder
zum ersten Mal in dieser Tiefe über dieses Detail nach. Auch da bin ich mir nicht 100% sicher.