C ++: ¿Es seguro lanzar el puntero a int y luego volver al puntero nuevamente?

¿Es seguro lanzar el puntero a int y luego volver al puntero nuevamente?

¿Qué tal si sabemos si el puntero tiene 32 bits de longitud e int tiene 32 bits de longitud?

long* juggle(long* p) { static_assert(sizeof(long*) == sizeof(int)); int v = reinterpret_cast(p); // or if sizeof(*)==8 choose long here do_some_math(v); // prevent compiler from optimizing return reinterpret_cast(v); } int main() { long* stuff = new long(42); long* ffuts = juggle(stuff); std::cout << "Is this always 42? " << *ffuts << std::endl; } 

¿Esto está cubierto por el estándar?

No.

Por ejemplo, en x86-64, un puntero es de 64 bits de longitud, pero int tiene solo 32 bits de longitud. Lanzar un puntero a int y viceversa hace que se pierdan los 32 bits superiores del valor del puntero.

Puede usar el tipo intptr_t en si desea un tipo entero que garantice que sea tan largo como el puntero. Puede reinterpretar_cast de manera segura desde un puntero a un intptr_t y viceversa.

Si y no.

La especificación del lenguaje establece explícitamente que es seguro (lo que significa que al final obtendrá el valor del puntero original) siempre que el tamaño del tipo integral sea suficiente para almacenar la representación integral [dependiente de la implementación] del puntero.

Entonces, en general, no es “seguro”, ya que, en general, int puede resultar demasiado pequeño. En su caso específico, aunque podría ser seguro, ya que su int podría ser lo suficientemente grande como para almacenar su puntero.

Normalmente, cuando necesite hacer algo como eso, debe usar los tipos intptr_t / uintptr_t , que se introducen específicamente para ese fin. Lamentablemente, intptr_t / uintptr_t no forman parte del estándar actual de C ++ (son tipos estándar C99), pero muchas implementaciones los proporcionan de todos modos. Siempre puede definir estos tipos usted mismo, por supuesto.

Sí, si … (o “Sí, pero …”) y no de otra manera.

La norma especifica (3.7.4.3) lo siguiente:

  • Un puntero es un puntero derivado de forma segura […] si es el resultado de una conversión de puntero bien definida o una reinterpret_cast de un valor de puntero derivado de forma segura [o] el resultado de una reinterpret_cast de una representación entera de un valor seguro -derive el valor del puntero
  • Un valor entero es una representación entera de un puntero derivado de forma segura […] si su tipo es al menos tan grande como std::intptr_t […] y el resultado de una reinterpret_cast de un valor de puntero derivado de manera segura [ o] el resultado de una conversión válida de una representación entera de un valor de puntero derivado de forma segura [o] el resultado de una operación aditiva o en modo bit, uno de cuyos operandos es una representación entera de un valor de puntero derivado de forma segura
  • Un objeto puntero trazable es un […] objeto de un tipo integral que es al menos tan grande como std::intptr_t

El estándar además establece que las implementaciones pueden ser relajadas o estrictas para aplicar punteros derivados de forma segura. Lo que significa que no se especifica si el uso o la eliminación de referencias de un puntero derivado de manera no segura invoca un comportamiento indefinido (¡eso es algo gracioso!)

Lo que en conjunto significa nada más y nada menos que “algo diferente podría funcionar de todos modos, pero la única cosa segura es la especificada anteriormente”.

Por lo tanto, si usa std::intptr_t en primer lugar (¡lo más recomendable!) O si sabe que el tamaño de almacenamiento del tipo entero que use (por ejemplo, long ) es al menos el tamaño de std::intptr_t , entonces está permitido y bien definido (es decir, “seguro”) para convertir a su tipo entero y viceversa. El estándar garantiza eso.

Si ese no es el caso, la conversión de puntero a representación entera probablemente (o al menos posiblemente) pierda cierta información, y la conversión de vuelta no dará un puntero válido. O bien, podría ser por accidente, pero esto no está garantizado.

Una anécdota interesante es que el estándar de C ++ no define directamente std::intptr_t ; simplemente dice “lo mismo que 7.18 en el estándar C” .

El estándar C, por otro lado, declara que “designa un tipo de entero con signo con la propiedad de que cualquier puntero válido a void se puede convertir a este tipo, luego se convierte de nuevo a puntero en void, y el resultado se comparará con el puntero original “ .
Lo que significa que, sin las definiciones algo complicadas anteriores (en particular, el último bit del primer punto), no se permitiría convertir a / de nada más que void* .

En general, no; los punteros pueden ser más grandes que int , en cuyo caso no hay forma de reconstruir el valor.

Si se sabe que un tipo entero es lo suficientemente grande, entonces usted puede; de acuerdo con la Norma (5.2.10 / 5):

Un puntero convertido a un número entero de tamaño suficiente … y de vuelta al mismo tipo de puntero tendrá su valor original

Sin embargo, en C ++ 03, no hay una forma estándar de decir qué tipos de enteros son lo suficientemente grandes. C ++ 11 y C99 (y por lo tanto, en la práctica, la mayoría de las implementaciones de C ++ 03), y también Boost.Integer, definen intptr_t y uintptr_t para este propósito. O puede definir su propio tipo y afirmar (preferiblemente en tiempo de comstackción) que es lo suficientemente grande; o, si no tiene alguna razón especial para que sea un tipo entero, use void* .

¿Es seguro? Realmente no.

En la mayoría de las circunstancias, ¿funcionará? Sí

Ciertamente, si un int es demasiado pequeño para mantener el valor del puntero completo y trunca, no recuperará el puntero original (con suerte su comstackdor le advertirá sobre este caso, con las conversiones truncadas de puntero a enteros de GCC son errores duros). Una uintptr_t long , o uintptr_t si su biblioteca lo admite, puede ser una mejor opción.

Incluso si el tipo entero y los tipos de puntero son del mismo tamaño, no necesariamente funcionará según el tiempo de ejecución de la aplicación. En particular, si está utilizando un recolector de basura en su progtwig, puede decidir fácilmente que el puntero ya no esté sobresaliente, y cuando posteriormente vuelva a convertir su entero en un puntero y trate de quitarle la referencia, descubrirá el objeto ya fue cosechado.

Absolutamente no. Hacer algunos hace una mala suposición de que el tamaño de un int y un puntero son los mismos. Esto casi siempre no es el caso en las plataformas de 64 bits. Si no son lo mismo, se producirá una pérdida de precisión y el valor del puntero final será incorrecto.

 MyType* pValue = ... int stored = (int)pValue; // Just lost the upper 4 bytes on a 64 bit platform pValue = (MyType*)stored; // pValue is now invalid pValue->SomeOp(); // Kaboom 

No, no es (siempre) seguro (por lo tanto no es seguro en general). Y está cubierto por el estándar.

ISO C ++ 2003, 5.2.10:

  1. Un puntero puede convertirse explícitamente en cualquier tipo integral lo suficientemente grande como para contenerlo . La función de mapeo está definida por la implementación.
  2. Un valor de tipo integral o tipo de enumeración puede convertirse explícitamente en un puntero. Un puntero convertido a un número entero de tamaño suficiente ( si existe alguno en la implementación) y de vuelta al mismo tipo de puntero tendrá su valor original; las asignaciones entre los punteros y los enteros son, de lo contrario, definidas por la implementación.

(Los énfasis anteriores son míos)

Por lo tanto, si sabe que los tamaños son compatibles, entonces la conversión es segura.

 #include  // C++03 static_assert. #define ASSURE(cond) typedef int ASSURE[(cond) ? 1 : -1] // Assure that the sizes are compatible. ASSURE(sizeof (int) >= sizeof (char*)); int main() { char c = 'A'; char *p = &c; // If this program compiles, it is well formed. int i = reinterpret_cast(p); p = reinterpret_cast(i); std::cout < < *p << std::endl; } 

Use uintptr_t desde “stdint.h” o desde “boost / stdint.h”. Se garantiza que tiene suficiente espacio de almacenamiento para un puntero.

No, no es. Incluso si descartamos el problema de la architecture, el tamaño de un puntero y un número entero tienen diferencias. Un puntero puede ser de tres tipos en C ++: cerca, lejos y enorme. Ellos tienen diferentes tamaños. Y si hablamos de un entero, normalmente es de 16 o 32 bits. Así que convertir un entero en punteros y viceversa no es seguro. Se debe tener mucho cuidado, ya que hay muchas posibilidades de pérdida de precisión. En la mayoría de los casos, un entero tendrá poco espacio para almacenar un puntero, lo que resulta en una pérdida de valor.

Si va a hacer algún casting portátil del sistema, necesita usar algo como Microsofts INT_PTR / UINT_PTR , la seguridad después de eso depende de las plataformas de destino y de lo que pretenda hacer con INT_PTR. en general, para la mayoría de los caracteres aritmáticos * o uint_8 * funciona mejor al ser seguro de tipo (ish)

Para un int? no siempre si está en una máquina de 64 bits, entonces int solo tiene 4 bytes; sin embargo, los punteros tienen 8 bytes de longitud y, por lo tanto, terminaría con un puntero diferente cuando lo vuelva a lanzar desde int.

Sin embargo, hay formas de evitar esto. Simplemente puede usar un tipo de datos de 8 bytes de longitud, que funcionaría ya sea que esté o no en un sistema de 32/64 bits, como unsigned long long sin firmar porque no desea la extensión de la señal en los sistemas de 32 bits.

Es importante tener en cuenta que en Linux unsigned long siempre tendrá el tamaño del puntero *, por lo que si está apuntando a sistemas Linux, puede usarlo.

* Según cppreference y también lo probé yo mismo, pero no en todos los sistemas Linux y Linux.

Si el problema es que quieres hacer cálculos matemáticos normales, probablemente lo más seguro sería convertirlo en un puntero a char (o mejor aún, * uint8_t ), hacer tus cálculos y luego devolverlo.