Variación sobre el tipo de tema: construcción trivial en el lugar

9

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)

espectros
fuente
2
Pero contiguo Pixels colocado nuevo todavía no es una matriz de Pixels.
Jarod42
1
@spectras Sin embargo, eso no hace una matriz. Solo tienes un montón de objetos Pixel uno al lado del otro. Eso es diferente de una matriz.
NathanOliver
1
Entonces no, ¿dónde haces pixels[some_index]o *(pixels + something)? Eso sería UB.
NathanOliver
1
La sección relevante está aquí y la frase clave es si P apunta a un elemento de matriz i de un objeto de matriz x . Aquí pixels(P) no es un puntero al objeto de matriz, sino un puntero a un solo Pixel. Eso significa que solo puede acceder pixels[0]legalmente.
NathanOliver
3
Desea leer wg21.link/P0593 .
ecatmur

Respuestas:

3

Es un comportamiento indefinido usar el resultado promotecomo una matriz. Si miramos [expr.add] /4.2 tenemos

De lo contrario, si Papunta a un elemento ide matriz de un objeto de matrizx con nelementos ([dcl.array]), las expresiones P + Jy J + P(donde Jtiene el valor j) apuntan al elemento i+jde matriz (posiblemente hipotético) de xif 0≤i+j≤ny la expresión P - Japunta a ( posiblemente-hipotético) elemento i−jde matriz de xif 0≤i−j≤n.

vemos 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 lo Pixelssigue 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.

NathanOliver
fuente
Gracias por descubrir eso rápido. Voy a hacer un iterador en su lugar, supongo. Como nota al margen, esto también significa que &somevector[0] + 1es UB (bueno, quiero decir, usar el puntero resultante sería).
espectras
@spectras Eso está realmente bien. Siempre puede hacer que el puntero pase a un objeto. Simplemente no puede desreferenciar ese puntero, incluso si hay un objeto válido allí.
NathanOliver
Sí, edité el comentario para aclararme, quise hacer referencia al puntero resultante :) Gracias por confirmar.
espectros
@spectras No hay problema. Esta parte de C ++ puede ser muy difícil. A pesar de que el hardware hará lo que queremos que haga, en realidad eso no es lo que codificamos. Estamos codificando la máquina abstracta C ++ y es una máquina persnickety;) Esperemos que P0593 sea adoptado y esto sea mucho más fácil.
NathanOliver
1
@spectras No, porque un vector estándar se define como que contiene una matriz, y puede hacer una aritmética de puntero entre los elementos de la matriz. Lamentablemente, no hay forma de implementar el vector estándar en C ++, sin encontrarse con UB.
Yakk - Adam Nevraumont
1

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 primero Pixel:

Se reinterpret_castrealiza antes de Pixelcrear cualquier objeto (suponiendo que no lo haga getSomeImageData). Por reinterpret_castlo tanto , no cambiará el valor del puntero. El puntero resultante todavía apuntará al primer elemento de la std::bytematriz pasado a la función.

Cuando cree los Pixelobjetos, se anidarán dentro de la std::bytematriz y la std::bytematriz proporcionará almacenamiento para los Pixelobjetos.

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á al std::byteobjeto, no al Pixelobjeto. Supongo que usarlo como si estuviera apuntando a un Pixelobjeto técnicamente será un comportamiento indefinido.

Creo que esto aún se mantiene, incluso si lo hace reinterpret_castdespués de crear el Pixelobjeto, ya que el Pixelobjeto y el std::byteque le proporciona almacenamiento no son interconvertibles por puntero . Entonces, incluso entonces, el puntero seguiría apuntando al objeto std::byte, no al Pixelobjeto.

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 adecuadamente Pixely que la matriz sea realmente lo suficientemente grande. Por lo que recuerdo, el estándar realmente no requiere que Pixeltenga la misma alineación std::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::byte matriz tenga el tamaño suficiente y esté alineada adecuadamente para los Pixelobjetos.

nuez
fuente
Creo que eso es correcto. Incluso si la cosa array (unimplementability de std::vector) no era un problema, todavía necesitaría std::launderel resultado antes de acceder a cualquiera de los orientados por ubicación newed Pixels. A partir de ahora, std::launderaquí está UB, ya que los Pixels adyacentes serían accesibles desde el puntero lavado .
Fureeish
@Fureeish No estoy seguro de por qué std::laundersería UB si se aplica resultantes de regresar. El adyacente Pixelno 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 la std::bytematriz original es accesible desde el puntero original.
nogal
Pero el siguiente Pixelno será accesible desde el std::bytepuntero, sino desde el launderpuntero ed. Creo que esto es relevante aquí. Sin embargo, estoy feliz de ser corregido.
Fureeish
@Fureeish Por lo que puedo decir, ninguno de los ejemplos dados se aplica aquí y la definición del requisito también dice lo mismo que el estándar. La accesibilidad se define en términos de bytes de almacenamiento, no de objetos. El byte ocupado por el siguiente me Pixelparece accesible desde el puntero original, porque el puntero original apunta a un elemento de la std::bytematriz que contiene los bytes que componen el almacenamiento para Pixelhacer el " o dentro de la matriz que encierra inmediatamente de la cual Z es un se aplica la condición " elemento " (donde Zestá Y, es decir, el std::byteelemento mismo).
nogal
Sin Pixelembargo, creo que los bytes de almacenamiento que ocupa el siguiente no son accesibles a través del puntero lavado, porque el Pixelobjeto 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 detalle std::launderpor primera vez en esa profundidad. Tampoco estoy 100% seguro de esto.
nogal