Wariacja na temat typowania znaków: trywialna konstrukcja na miejscu

9

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 Pixeltypy 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)

widma
źródło
2
Ale ciągłe Pixels umieszczone nowe wciąż nie jest tablicą Pixels.
Jarod42
1
@spectras To nie tworzy tablicy. Po prostu masz kilka obiektów Pixel obok siebie. To różni się od tablicy.
NathanOliver
1
Więc nie ma gdzie zrobić, pixels[some_index]albo *(pixels + something)? To byłby UB.
NathanOliver
1
Odpowiednia sekcja znajduje się tutaj, a kluczową frazą jest, jeśli P wskazuje element tablicy i obiektu tablicy x . Tutaj pixels(P) nie jest wskaźnikiem do obiektu tablicy, ale wskaźnikiem do pojedynczego Pixel. Oznacza to, że masz dostęp tylko pixels[0]legalnie.
NathanOliver
3
Chcesz przeczytać wg21.link/P0593 .
ecatmur

Odpowiedzi:

3

Nieokreślonym zachowaniem jest używanie wyniku promotejako jako tablicy. Jeśli spojrzymy na [expr.add] / 4.2 mamy

W przeciwnym razie, jeżeli Pwskazuje na element z matrycą iobiektu tablicyx z nelementów ([dcl.array]), wyrażenia P + Ji J + P(jeśli Jma wartość j) Temperatura do (ewentualnie), hipotetycznego elementu tablicy i+jw xrazie 0≤i+j≤noraz ekspresję P - Jpunktów do ( ewentualnie, hipotetyczny) matrycy elementów i−jz xjeśli 0≤i−j≤n.

widzimy, że wskaźnik musi wskazywać obiekt tablicy. Jednak tak naprawdę nie masz obiektu tablicowego. Masz wskaźnik do jednego, Pixelktóry po prostu zdarza się, że inny Pixelspodąż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.

NathanOliver
źródło
Dzięki, że dowiedziałeś się o tym tak szybko. Zamiast tego zrobię iterator. Jako sidenote oznacza to również, że &somevector[0] + 1jest to UB (cóż, mam na myśli użycie wynikowego wskaźnika).
spectras
@spectras To jest właściwie w porządku. Zawsze możesz uzyskać wskaźnik do jednego obiektu. Po prostu nie możesz wyłuskać tego wskaźnika, nawet jeśli jest tam prawidłowy obiekt.
NathanOliver
Tak, zredagowałem komentarz, aby się wyjaśnić, miałem na myśli dereferencję wynikowego wskaźnika :) Dziękuję za potwierdzenie.
spectras
@spectras Nie ma problemu. Ta część C ++ może być bardzo trudna. Mimo że sprzęt zrobi to, co chcemy, nie jest to właściwie kodowanie. Kodujemy do maszyny abstrakcyjnej C ++ i jest to maszyna persnickety;) Mam nadzieję, że P0593 zostanie adoptowana i stanie się to znacznie łatwiejsze.
NathanOliver
1
@spectras Nie, ponieważ wektor std jest zdefiniowany jako zawierający tablicę i można wykonywać arytmetykę wskaźników między elementami tablicy. Niestety, nie można zaimplementować wektora std w samym C ++ bez uruchamiania w UB.
Yakk - Adam Nevraumont
1

Masz już odpowiedź dotyczącą ograniczonego użycia zwróconego wskaźnika, ale chcę dodać, że myślę, że musisz mieć std::laundernawet dostęp do pierwszego Pixel:

reinterpret_castOdbywa się przed każdym Pixeltworzony jest obiekt (zakładając, że nie robią tego w getSomeImageData). Dlatego reinterpret_castnie zmieni wartości wskaźnika. Wynikowy wskaźnik nadal będzie wskazywać pierwszy element std::bytetablicy przekazany do funkcji.

Podczas tworzenia Pixelobiektów, mają zamiar być zagnieżdżona w std::bytetablicy, a std::bytetablica będzie zapewnienie składowania dla Pixelobiektó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 resultnadal wskaże std::byteobiekt, a nie Pixelprzedmiot. Wydaje mi się, że użycie go tak, jakby wskazywał na Pixelobiekt, będzie technicznie zachowaniem niezdefiniowanym.

Myślę, że nadal tak jest, nawet jeśli robisz to reinterpret_castpo utworzeniu Pixelobiektu, ponieważ Pixelobiekt i obiekt, std::bytektóry zapewnia dla niego miejsce do przechowywania, nie są wzajemnie wymienialne . Nawet wtedy wskaźnik nadal wskazywałby na obiekt std::byte, a nie na Pixelobiekt.

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 Pixelobiektu.


Musisz także upewnić się, że std::bytewskaźnik jest odpowiednio wyrównany Pixeli że tablica naprawdę jest wystarczająco duża. O ile pamiętam, standard tak naprawdę nie wymaga takiego Pixelsamego wyrównania, jak std::bytei tego, że nie ma wypełnienia.


Nic z tego nie zależy od Pixelbycia trywialnym lub jakiejkolwiek innej jego własności. Wszystko zachowywałoby się w ten sam sposób, o ile std::bytetablica ma wystarczającą wielkość i jest odpowiednio dopasowana do Pixelobiektów.

orzech włoski
źródło
Myślę, że to prawda. Nawet jeśli rzecz array (unimplementability z std::vector) nie było problemu, to trzeba jeszcze std::launderwyniku przed uzyskaniem dostępu do któregokolwiek z placement- newed Pixels. Na chwilę obecną std::launderjest UB, ponieważ sąsiednie litery Pixels byłyby osiągalne z wypranego wskaźnika.
Fureeish
@ Fureeish Nie jestem pewien, dlaczego std::laundermiałbym być UB, gdyby został zgłoszony resultprzed powrotem. Sąsiednie Pixelnie 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 oryginalna std::bytetablica jest dostępna z oryginalnego wskaźnika.
orzech
Ale następny Pixelnie będzie osiągalny ze std::bytewskaźnika, ale jest ze launderwskaźnika ed. Wierzę, że to jest istotne tutaj. Ale cieszę się, że mnie poprawiono.
Fureeish
@Fureeish Z tego, co mogę powiedzieć, żaden z podanych przykładów nie ma tutaj zastosowania, a definicja wymagania mówi również to samo co norma. Osiągalność jest definiowana w kategoriach bajtów pamięci, a nie obiektów. Bajt zajmowany przez następny Pixelwydaje mi się osiągalny z oryginalnego wskaźnika, ponieważ oryginalny wskaźnik wskazuje na element std::bytetablicy, który zawiera bajty tworzące pamięć do Pixelutworzenia „ lubw obrębie bezpośrednio otaczającej tablicy, której Z jest element ”mają zastosowanie warunki (gdzie Zjest Y, tzn. std::bytesam element).
orzech
Myślę, że bajty pamięci, które Pixelzajmują następne, nie są osiągalne przez wyprany wskaźnik, ponieważ wskazany Pixelobiekt 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óle std::launderpo raz pierwszy w tej głębi. Nie jestem tego w 100% pewien.
orzech