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
Pixel
tipos 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::byte
que la pregunta se mantiene usando char
)
Pixel
s colocado nuevo todavía no es una matriz dePixel
s.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
promote
como 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
Pixel
que simplemente sucede que otro loPixels
sigue 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] + 1
es 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::launder
poder acceder al primeroPixel
:Se
reinterpret_cast
realiza antes dePixel
crear cualquier objeto (suponiendo que no lo hagagetSomeImageData
). Porreinterpret_cast
lo tanto , no cambiará el valor del puntero. El puntero resultante todavía apuntará al primer elemento de lastd::byte
matriz pasado a la función.Cuando cree los
Pixel
objetos, se anidarán dentro de lastd::byte
matriz y lastd::byte
matriz proporcionará almacenamiento para losPixel
objetos.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
result
lo que todavía apuntará alstd::byte
objeto, no alPixel
objeto. Supongo que usarlo como si estuviera apuntando a unPixel
objeto técnicamente será un comportamiento indefinido.Creo que esto aún se mantiene, incluso si lo hace
reinterpret_cast
después de crear elPixel
objeto, ya que elPixel
objeto y elstd::byte
que le proporciona almacenamiento no son interconvertibles por puntero . Entonces, incluso entonces, el puntero seguiría apuntando al objetostd::byte
, no alPixel
objeto.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
Pixel
objeto específico .También debe asegurarse de que el
std::byte
puntero esté alineado adecuadamentePixel
y que la matriz sea realmente lo suficientemente grande. Por lo que recuerdo, el estándar realmente no requiere quePixel
tenga la misma alineaciónstd::byte
o que no tenga relleno.Además, nada de esto depende de
Pixel
ser trivial o realmente de cualquier otra propiedad del mismo. Todo se comportaría de la misma manera mientrasstd::byte
matriz tenga el tamaño suficiente y esté alineada adecuadamente para losPixel
objetos.fuente
std::vector
) no era un problema, todavía necesitaríastd::launder
el resultado antes de acceder a cualquiera de los orientados por ubicaciónnew
edPixel
s. A partir de ahora,std::launder
aquí está UB, ya que losPixel
s adyacentes serían accesibles desde el puntero lavado .std::launder
sería UB si se aplicaresult
antes de regresar. El adyacentePixel
no 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::byte
matriz original es accesible desde el puntero original.Pixel
no será accesible desde elstd::byte
puntero, sino desde ellaunder
puntero ed. Creo que esto es relevante aquí. Sin embargo, estoy feliz de ser corregido.Pixel
parece accesible desde el puntero original, porque el puntero original apunta a un elemento de lastd::byte
matriz que contiene los bytes que componen el almacenamiento paraPixel
hacer el " o dentro de la matriz que encierra inmediatamente de la cual Z es un se aplica la condición " elemento " (dondeZ
estáY
, es decir, elstd::byte
elemento mismo).Pixel
embargo, creo que los bytes de almacenamiento que ocupa el siguiente no son accesibles a través del puntero lavado, porque elPixel
objeto 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::launder
por primera vez en esa profundidad. Tampoco estoy 100% seguro de esto.