c opengl pointers null pointer-arithmetic

¿Cuál es el resultado de NULL+int?



opengl pointers (4)

He visto que se usa la siguiente macro en las implementaciones de OpenGL VBO:

#define BUFFER_OFFSET(i) ((char *)NULL + (i)) //... glNormalPointer(GL_FLOAT, 32, BUFFER_OFFSET(x));

¿Podría proporcionar un pequeño detalle sobre cómo funciona esta macro? ¿Se puede reemplazar con una función? Más exactamente, ¿cuál es el resultado de incrementar un puntero NULL?


Hagamos un viaje de regreso a través de la historia sórdida de OpenGL. Había una vez OpenGL 1.0. glBegin y glEnd para hacer dibujos, y eso fue todo. Si quería un dibujo rápido, pega cosas en una lista de visualización.

Luego, alguien tuvo la brillante idea de poder tomar arrays de objetos para procesar. Y así nació OpenGL 1.1, que nos trajo funciones como glVertexPointer . Puede observar que esta función termina en la palabra "Puntero". Esto se debe a que toma punteros a la memoria real, a la que se accederá cuando se glDraw* una de las glDraw* de glDraw* .

Avance rápido unos años más. Ahora, la gente quiere poner datos de vértice en la memoria de la GPU, pero no quieren confiar en las listas de visualización. Esos están demasiado ocultos, y no hay manera de saber si obtendrás un buen rendimiento con ellos. Ingrese los objetos de buffer.

Sin embargo, debido a que el ARB tenía una política absoluta de hacer que todo fuera lo más compatible posible (sin importar cuán tonto fuera el aspecto de la API), decidieron que la mejor manera de implementar esto era utilizar de nuevo las mismas funciones. Solo ahora, hay un interruptor global que cambia el comportamiento de glVertexPointer de "toma un puntero" a "toma un desplazamiento de bytes de un objeto de memoria intermedia". Ese cambio es si un objeto buffer está o no obligado a GL_ARRAY_BUFFER .

Por supuesto, en lo que respecta a C / C ++, la función aún toma un puntero . Y las reglas de C / C ++ no le permiten pasar un entero como un puntero. No sin un elenco. Por eso existen macros como BUFFER_OBJECT . Es una manera de convertir su desplazamiento de bytes enteros en un puntero.

La parte (char *)NULL simplemente toma el puntero NULL (que normalmente es un void* ) y lo convierte en un char* . El + i solo hace la aritmética del puntero en el char* . Como NULL suele ser un valor cero, al sumar i aumentará el desplazamiento del byte en i , generando así un puntero cuyo valor es el desplazamiento del byte que pasó.

Por supuesto, la especificación de C ++ enumera los resultados de BUFFER_OBJECT como un comportamiento indefinido . Al usarlo, realmente está confiando en el compilador para hacer algo razonable. Después de todo, NULL no tiene que ser cero; toda la especificación dice que es una constante de puntero nulo definido por la implementación. No tiene que tener el valor de cero en absoluto. En la mayoría de los sistemas reales, lo hará. Pero no tiene por qué.

Es por eso que solo uso un elenco.

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48);

Es un comportamiento indefinido de cualquier manera. Pero también es más corto que escribir "BUFFER_OFFSET". GCC y Visual Studio parecen encontrarlo razonable. Y no se basa en el valor de la macro NULL.

Personalmente, si fuera más pedante de C ++, usaría un reinterpret_cast<void*> en él. Pero yo no.

¿Puedes escribir un glVertexAttribBuffer que tome un desplazamiento en lugar de un puntero? Absolutamente. Pero no es un gran problema. Solo haz un yeso.


Los datos de atributo de vértice de OpenGL se asignan a través de la misma función (glVertexAttribPointer) como punteros en la memoria o desplazamientos ubicados dentro de un Objeto de búfer de Vértices, según el contexto.

la macro BUFFER_OFFSET () parece convertir un desplazamiento de bytes entero en un puntero simplemente para permitir que el compilador lo pase como un argumento de puntero de forma segura. El "(char *) NULL + i" expresa esta conversión a través de puntero-aritmética; el resultado debería ser el mismo patrón de bits suponiendo sizeof (char) == 1, sin el cual, esta macro fallaría.

también sería posible mediante un simple reencasting, pero la macro podría hacer que sea estilísticamente más claro lo que se está pasando; también sería un lugar conveniente para atrapar los desbordamientos para la seguridad de 32/64 bits / a prueba de futuro

struct MyVertex { float pos[3]; u8 color[4]; } // general purpose Macro to find the byte offset of a structure member as an ''int'' #define OFFSET(TYPE, MEMBER) ( (int)&((TYPE*)0)->MEMBER) // assuming a VBO holding an array of ''MyVertex'', // specify that color data is held at an offset 12 bytes from the VBO start, for every vertex. glVertexAttribPointer( colorIndex,4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(MyVertex), (GLvoid*) OFFSET(MyVertex, color) // recast offset as pointer );


Eso no es "NULL + int", es un "lanzamiento NULL al tipo ''puntero a char''", y luego incrementa ese puntero en i.

Y sí, eso podría ser reemplazado por una función, pero si no sabes lo que hace, entonces ¿por qué te importa eso? Primero entiende lo que hace, luego considera si sería mejor como una función.


#define BUFFER_OFFSET(i) ((char *)NULL + (i))

Técnicamente, el resultado de esta operación no está definido y la macro realmente incorrecta. Dejame explicar:

C define (y C ++ lo sigue), que los punteros se pueden convertir en enteros, es decir, de tipo uintptr_t , y que si el entero obtenido de esa manera, fundido de nuevo en el tipo de puntero original del que proviene, arrojaría el puntero original.

Luego está la aritmética del puntero, lo que significa que si tengo dos punteros apuntando para que el mismo objeto pueda tomar la diferencia de ellos, lo que da como resultado un entero (del tipo ptrdiff_t ), y ese entero agregado o restado a cualquiera de los punteros originales, dará como resultado el otro. También se define que, al agregar 1 a un puntero, se obtiene el puntero al siguiente elemento de un objeto indexado. También la diferencia de dos uintptr_t , dividida por sizeof(type pointed to) de punteros del mismo objeto debe ser igual a los punteros que se restan. Y por último pero no menos importante, los valores de uintptr_t pueden ser cualquier cosa. Podrían ser mangos opacos también. No se requiere que sean las direcciones (aunque la mayoría de las implementaciones lo hacen de esa manera, porque tiene sentido).

Ahora podemos mirar el infame puntero nulo. C define el puntero que se convierte a para desde el tipo uintptr_u valor 0 como el puntero no válido. Tenga en cuenta que esto siempre es 0 en su código fuente. En el lado de back-end, en el programa compilado, ¡el valor binario utilizado para representarlo en la máquina puede ser algo completamente diferente! Usualmente no lo es, pero puede ser. C ++ es el mismo, pero C ++ no permite tanta conversión implícita que C, por lo que uno debe convertir 0 explícitamente en void* . Además, dado que el puntero nulo no se refiere a un objeto y, por lo tanto, no tiene un tamaño desreferenciado, la aritmética del puntero no está definida para el puntero nulo . El puntero nulo que hace referencia a ningún objeto también significa que no existe una definición para convertirlo con sensatez en un puntero tipeado.

Entonces, si todo esto no está definido, ¿por qué funciona esta macro después de todo? Porque la mayoría de las implementaciones (compiladores de medios) son extremadamente crédulas y los codificadores de compiladores perezosos al más alto nivel. El valor entero de un puntero en la mayoría de las implementaciones es solo el valor del puntero en el lado del back-end. Entonces, el puntero nulo es en realidad 0. Y aunque no se verifica la aritmética del puntero en el puntero nulo, la mayoría de los compiladores lo aceptarán en silencio, si el puntero obtuvo algún tipo asignado, incluso si no tiene sentido. char es el tipo de "tamaño de unidad" de C si quieres decirlo. Entonces, la aritmética del puntero en el molde es como artihmetic en las direcciones en el lado del back-end.

Para abreviar, simplemente no tiene sentido intentar hacer magia de puntero con el resultado previsto como una compensación en el lado del lenguaje C, simplemente no funciona de esa manera.

Retrocedamos por un momento y recordemos, lo que en realidad estamos tratando de hacer: el problema original era que las gl…Pointer funciones del gl…Pointer toman un puntero como su parámetro de datos, pero para Objetos del búfer de vértices realmente queremos especificar un byte basado compensar en nuestros datos, que es un número. Para el compilador de C la función toma un puntero (una cosa opaca como aprendimos). La solución correcta habría sido la introducción de nuevas funciones especialmente para el uso con VBO (digamos gl…Offset - Creo que voy a ralley para su presentación). En cambio, lo que OpenGL definió es una explotación de cómo funcionan los compiladores. Los punteros y su equivalente entero se implementan como la misma representación binaria por la mayoría de los compiladores. Entonces, lo que tenemos que hacer es hacer que el compilador llame a esas gl…Pointer Funciones de gl…Pointer con nuestro número en lugar de un puntero.

Entonces, técnicamente, lo único que tenemos que hacer es decirle al compilador "sí, sé que piensas que esta variable a es un número entero, y tienes razón, y que la función glVertexPointer solo toma un void* por su parámetro de datos. Pero adivina qué: Ese entero se obtuvo de un void* ", al convertirlo en (void*) y luego pulgares, que el compilador en realidad es tan estúpido para pasar el valor entero como lo hace a glVertexPointer .

Así que todo esto se reduce a de alguna manera eludir la antigua firma de función. Lanzar el puntero es el método sucio de IMHO. Lo haría un poco diferente: me metería con la firma de la función:

typedef void (*TFPTR_VertexOffset)(GLint, GLenum, GLsizei, uintptr_t); TFPTR_VertexOffset myglVertexOffset = (TFPTR_VertexOffset)glVertexPointer;

Ahora puede usar myglVertexOffset sin hacer ningún moldeado tonto, y el parámetro de desplazamiento se pasará a la función, sin ningún peligro, para que el compilador pueda meterse con él. Este es también el mismo método que uso en mis programas.