Sé que este es un tema bastante común, pero aunque la UB típica es fácil de encontrar, no encontré esta variante hasta ahora.
Por lo tanto, estoy tratando de presentar formalmente los objetos Pixel mientras evito una copia real de los datos.
¿Es esto válido?
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.
}
Patrón de uso esperado, muy simplificado:
std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data
Más específicamente:
- ¿Este código tiene un comportamiento bien definido?
- En caso afirmativo, ¿es seguro usar el puntero devuelto?
- En caso afirmativo, ¿a qué otros
Pixeltipos se puede extender? (¿relajando la restricción is_trivial? ¿píxel con solo 3 componentes?).
Tanto clang como gcc optimizan todo el ciclo a la nada, que es lo que quiero. Ahora, me gustaría saber si esto viola algunas reglas de C ++ o no.
Enlace Godbolt si quieres jugar con él.
(nota: no etiqueté c ++ 17 a pesar de std::byteque la pregunta se mantiene usando char)

Pixels colocado nuevo todavía no es una matriz dePixels.pixels[some_index]o*(pixels + something)? Eso sería UB.pixels(P) no es un puntero al objeto de matriz, sino un puntero a un soloPixel. Eso significa que solo puede accederpixels[0]legalmente.Respuestas:
Es un comportamiento indefinido usar el resultado
promotecomo una matriz. Si miramos [expr.add] /4.2 tenemosvemos que requiere que el puntero apunte realmente a un objeto de matriz. Sin embargo, en realidad no tienes un objeto de matriz. Tiene un puntero a un solo
Pixelque simplemente sucede que otro loPixelssigue en la memoria contigua. Eso significa que el único elemento al que realmente puede acceder es el primer elemento. Intentar acceder a cualquier otra cosa sería un comportamiento indefinido porque ha pasado el final del dominio válido para el puntero.fuente
&somevector[0] + 1es UB (bueno, quiero decir, usar el puntero resultante sería).Ya tiene una respuesta con respecto al uso limitado del puntero devuelto, pero quiero agregar que también creo que necesita
std::launderpoder acceder al primeroPixel:Se
reinterpret_castrealiza antes dePixelcrear cualquier objeto (suponiendo que no lo hagagetSomeImageData). Porreinterpret_castlo tanto , no cambiará el valor del puntero. El puntero resultante todavía apuntará al primer elemento de lastd::bytematriz pasado a la función.Cuando cree los
Pixelobjetos, se anidarán dentro de lastd::bytematriz y lastd::bytematriz proporcionará almacenamiento para losPixelobjetos.Hay casos en los que la reutilización del almacenamiento hace que un puntero al objeto antiguo apunte automáticamente al nuevo objeto. Pero esto no es lo que está sucediendo aquí, por
resultlo que todavía apuntará alstd::byteobjeto, no alPixelobjeto. Supongo que usarlo como si estuviera apuntando a unPixelobjeto técnicamente será un comportamiento indefinido.Creo que esto aún se mantiene, incluso si lo hace
reinterpret_castdespués de crear elPixelobjeto, ya que elPixelobjeto y elstd::byteque le proporciona almacenamiento no son interconvertibles por puntero . Entonces, incluso entonces, el puntero seguiría apuntando al objetostd::byte, no alPixelobjeto.Si obtuvo el puntero para regresar del resultado de una de las ubicaciones nuevas, entonces todo debería estar bien, en lo que respecta al acceso a ese
Pixelobjeto específico .También debe asegurarse de que el
std::bytepuntero esté alineado adecuadamentePixely que la matriz sea realmente lo suficientemente grande. Por lo que recuerdo, el estándar realmente no requiere quePixeltenga la misma alineaciónstd::byteo que no tenga relleno.Además, nada de esto depende de
Pixelser trivial o realmente de cualquier otra propiedad del mismo. Todo se comportaría de la misma manera mientrasstd::bytematriz tenga el tamaño suficiente y esté alineada adecuadamente para losPixelobjetos.fuente
std::vector) no era un problema, todavía necesitaríastd::launderel resultado antes de acceder a cualquiera de los orientados por ubicaciónnewedPixels. A partir de ahora,std::launderaquí está UB, ya que losPixels adyacentes serían accesibles desde el puntero lavado .std::laundersería UB si se aplicaresultantes de regresar. El adyacentePixelno es " accesible " a través del puntero lavado, según tengo entendido de eel.is/c++draft/ptr.launder#4 . E incluso fue que no veo cómo es UB, porque toda lastd::bytematriz original es accesible desde el puntero original.Pixelno será accesible desde elstd::bytepuntero, sino desde ellaunderpuntero ed. Creo que esto es relevante aquí. Sin embargo, estoy feliz de ser corregido.Pixelparece accesible desde el puntero original, porque el puntero original apunta a un elemento de lastd::bytematriz que contiene los bytes que componen el almacenamiento paraPixelhacer el " o dentro de la matriz que encierra inmediatamente de la cual Z es un se aplica la condición " elemento " (dondeZestáY, es decir, elstd::byteelemento mismo).Pixelembargo, creo que los bytes de almacenamiento que ocupa el siguiente no son accesibles a través del puntero lavado, porque elPixelobjeto apuntado no es elemento de un objeto de matriz y tampoco es interconvertible por puntero con ningún otro objeto relevante. Pero también estoy pensando en este detallestd::launderpor primera vez en esa profundidad. Tampoco estoy 100% seguro de esto.