Wiem, że jest to dość powszechny temat, ale ponieważ typowy UB jest łatwy do znalezienia, do tej pory nie znalazłem tego wariantu.
Tak więc próbuję formalnie wprowadzić obiekty Pixel, unikając rzeczywistej kopii danych.
Czy to jest ważne?
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.
}
Oczekiwany wzór użycia, znacznie uproszczony:
std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data
Dokładniej:
- Czy ten kod ma dobrze zdefiniowane zachowanie?
- Jeśli tak, czy bezpiecznie jest używać zwróconego wskaźnika?
- Jeśli tak, na jakie inne
Pixel
typy można go rozszerzyć? (rozluźnianie ograniczenia is_trivial? piksel z tylko 3 składnikami?).
Zarówno clang, jak i gcc optymalizują całą pętlę do nicości, czego chcę. Teraz chciałbym wiedzieć, czy to narusza niektóre reguły C ++, czy nie.
Link Godbolt, jeśli chcesz się nim bawić.
(uwaga: mimo to nie oznaczyłem tagu c ++ 17 std::byte
, ponieważ pytanie to wymaga użycia char
)
Pixel
s umieszczone nowe wciąż nie jest tablicąPixel
s.pixels[some_index]
albo*(pixels + something)
? To byłby UB.pixels
(P) nie jest wskaźnikiem do obiektu tablicy, ale wskaźnikiem do pojedynczegoPixel
. Oznacza to, że masz dostęp tylkopixels[0]
legalnie.Odpowiedzi:
Nieokreślonym zachowaniem jest używanie wyniku
promote
jako jako tablicy. Jeśli spojrzymy na [expr.add] / 4.2 mamywidzimy, że wskaźnik musi wskazywać obiekt tablicy. Jednak tak naprawdę nie masz obiektu tablicowego. Masz wskaźnik do jednego,
Pixel
który po prostu zdarza się, że innyPixels
podąża za nim w ciągłej pamięci. Oznacza to, że jedynym dostępnym elementem jest pierwszy element. Próba uzyskania dostępu do czegokolwiek innego byłaby niezdefiniowanym zachowaniem, ponieważ przekroczono koniec prawidłowej domeny wskaźnika.źródło
&somevector[0] + 1
jest to UB (cóż, mam na myśli użycie wynikowego wskaźnika).Masz już odpowiedź dotyczącą ograniczonego użycia zwróconego wskaźnika, ale chcę dodać, że myślę, że musisz mieć
std::launder
nawet dostęp do pierwszegoPixel
:reinterpret_cast
Odbywa się przed każdymPixel
tworzony jest obiekt (zakładając, że nie robią tego wgetSomeImageData
). Dlategoreinterpret_cast
nie zmieni wartości wskaźnika. Wynikowy wskaźnik nadal będzie wskazywać pierwszy elementstd::byte
tablicy przekazany do funkcji.Podczas tworzenia
Pixel
obiektów, mają zamiar być zagnieżdżona wstd::byte
tablicy, astd::byte
tablica będzie zapewnienie składowania dlaPixel
obiektów.Są przypadki, w których ponowne użycie pamięci powoduje, że wskaźnik do starego obiektu automatycznie wskazuje nowy obiekt. Ale to nie dzieje się tutaj, więc
result
nadal wskażestd::byte
obiekt, a niePixel
przedmiot. Wydaje mi się, że użycie go tak, jakby wskazywał naPixel
obiekt, będzie technicznie zachowaniem niezdefiniowanym.Myślę, że nadal tak jest, nawet jeśli robisz to
reinterpret_cast
po utworzeniuPixel
obiektu, ponieważPixel
obiekt i obiekt,std::byte
który zapewnia dla niego miejsce do przechowywania, nie są wzajemnie wymienialne . Nawet wtedy wskaźnik nadal wskazywałby na obiektstd::byte
, a nie naPixel
obiekt.Jeśli uzyskałeś wskaźnik powrotu z wyniku jednego z umieszczenia-nowego, to wszystko powinno być w porządku, o ile dotyczy dostępu do tego konkretnego
Pixel
obiektu.Musisz także upewnić się, że
std::byte
wskaźnik jest odpowiednio wyrównanyPixel
i że tablica naprawdę jest wystarczająco duża. O ile pamiętam, standard tak naprawdę nie wymaga takiegoPixel
samego wyrównania, jakstd::byte
i tego, że nie ma wypełnienia.Nic z tego nie zależy od
Pixel
bycia trywialnym lub jakiejkolwiek innej jego własności. Wszystko zachowywałoby się w ten sam sposób, o ilestd::byte
tablica ma wystarczającą wielkość i jest odpowiednio dopasowana doPixel
obiektów.źródło
std::vector
) nie było problemu, to trzeba jeszczestd::launder
wyniku przed uzyskaniem dostępu do któregokolwiek z placement-new
edPixel
s. Na chwilę obecnąstd::launder
jest UB, ponieważ sąsiednie literyPixel
s byłyby osiągalne z wypranego wskaźnika.std::launder
miałbym być UB, gdyby został zgłoszonyresult
przed powrotem. SąsiedniePixel
nie jest „ osiągalne ” przez wypraną wskazówkę przechodzącą przez moje rozumienie eel.is/c++draft/ptr.launder#4 . I nawet tak nie było, jak to jest UB, ponieważ cała oryginalnastd::byte
tablica jest dostępna z oryginalnego wskaźnika.Pixel
nie będzie osiągalny zestd::byte
wskaźnika, ale jest zelaunder
wskaźnika ed. Wierzę, że to jest istotne tutaj. Ale cieszę się, że mnie poprawiono.Pixel
wydaje mi się osiągalny z oryginalnego wskaźnika, ponieważ oryginalny wskaźnik wskazuje na elementstd::byte
tablicy, który zawiera bajty tworzące pamięć doPixel
utworzenia „ lub ” w obrębie bezpośrednio otaczającej tablicy, której Z jest element ”mają zastosowanie warunki (gdzieZ
jestY
, tzn.std::byte
sam element).Pixel
zajmują następne, nie są osiągalne przez wyprany wskaźnik, ponieważ wskazanyPixel
obiekt nie jest elementem obiektu tablicowego, a także nie jest wymienny ze wskaźnikiem z żadnym innym odpowiednim obiektem. Ale myślę też o tym szczególestd::launder
po raz pierwszy w tej głębi. Nie jestem tego w 100% pewien.