¿Cuándo utilizar reinterpret_cast?
Estoy un poco confundido con la aplicabilidad de reinterpret_cast
vs. static_cast
Por lo que he leído, las reglas generales son usar conversión estática cuando los tipos se pueden interpretar en tiempo de compilación, de ahí la palabra static
. Esta es la conversión que el compilador de C++ usa internamente también para conversiones implícitas.
reinterpret_cast
s son aplicables en dos escenarios:
- convertir tipos de enteros a tipos de puntero y viceversa
- convertir un tipo de puntero a otro. La idea general que tengo es que esto no es portátil y debe evitarse.
Donde estoy un poco confundido es en un uso que necesito: estoy llamando a C++ desde C y el código C necesita retener el objeto C++, por lo que básicamente contiene un archivo void*
. ¿Qué conversión se debe utilizar para convertir entre el void *
tipo y la Clase?
¿He visto el uso de ambos static_cast
y reinterpret_cast
? Aunque por lo que he estado leyendo parece que static
es mejor ya que la conversión puede ocurrir en el momento de la compilación. ¿ Aunque dice usarlo reinterpret_cast
para convertir de un tipo de puntero a otro?
El estándar C++ garantiza lo siguiente:
static_cast
Al escribir un puntero hacia y desde void*
se conserva la dirección. Es decir, a continuación, a
y b
todos c
apuntan a la misma dirección:
int* a = new int();
void* b = static_cast<void*>(a);
int* c = static_cast<int*>(b);
reinterpret_cast
solo garantiza que si lanza un puntero a un tipo diferente y luego reinterpret_cast
lo vuelve al tipo original , obtendrá el valor original. Entonces en lo siguiente:
int* a = new int();
void* b = reinterpret_cast<void*>(a);
int* c = reinterpret_cast<int*>(b);
a
y c
contiene el mismo valor, pero el valor de b
no está especificado. (En la práctica, normalmente contendrá la misma dirección que a
y c
, pero eso no está especificado en el estándar y puede no ser cierto en máquinas con sistemas de memoria más complejos).
Para transmitir desde y hacia void*
, static_cast
se debe preferir.
Un caso en el que reinterpret_cast
es necesario es cuando se interactúa con tipos de datos opacos. Esto ocurre con frecuencia en API de proveedores sobre las cuales el programador no tiene control. A continuación se muestra un ejemplo artificial en el que un proveedor proporciona una API para almacenar y recuperar datos globales arbitrarios:
// vendor.hpp
typedef struct _Opaque * VendorGlobalUserData;
void VendorSetUserData(VendorGlobalUserData p);
VendorGlobalUserData VendorGetUserData();
Para utilizar esta API, el programador debe enviar sus datos hacia VendorGlobalUserData
y desde atrás. static_cast
no funcionará, uno debe usar reinterpret_cast
:
// main.cpp
#include "vendor.hpp"
#include <iostream>
using namespace std;
struct MyUserData {
MyUserData() : m(42) {}
int m;
};
int main() {
MyUserData u;
// store global data
VendorGlobalUserData d1;
// d1 = &u; // compile error
// d1 = static_cast<VendorGlobalUserData>(&u); // compile error
d1 = reinterpret_cast<VendorGlobalUserData>(&u); // ok
VendorSetUserData(d1);
// do other stuff...
// retrieve global data
VendorGlobalUserData d2 = VendorGetUserData();
MyUserData * p = 0;
// p = d2; // compile error
// p = static_cast<MyUserData *>(d2); // compile error
p = reinterpret_cast<MyUserData *>(d2); // ok
if (p) { cout << p->m << endl; }
return 0;
}
A continuación se muestra una implementación artificial de la API de muestra:
// vendor.cpp
static VendorGlobalUserData g = 0;
void VendorSetUserData(VendorGlobalUserData p) { g = p; }
VendorGlobalUserData VendorGetUserData() { return g; }
La respuesta corta:
si no sabes lo que reinterpret_cast
significa, no lo uses. Si lo necesitarás en el futuro, lo sabrás.
Respuesta completa:
Consideremos los tipos de números básicos.
Cuando convierte, por ejemplo, int(12)
a float (12.0f)
su procesador, necesita invocar algunos cálculos, ya que ambos números tienen una representación de bits diferente. Esto es lo que static_cast
representa.
Por otro lado, cuando llama reinterpret_cast
a la CPU no invoca ningún cálculo. Simplemente trata un conjunto de bits en la memoria como si tuvieran otro tipo. Entonces, cuando convierte int*
con float*
esta palabra clave, el nuevo valor (después de eliminar la referencia del puntero) no tiene nada que ver con el valor anterior en significado matemático (ignorando el hecho de que es un comportamiento indefinido leer este valor).
Tenga en cuenta que leer o modificar valores después de reinterprt_cast'ing suele ser un comportamiento indefinido . En la mayoría de los casos, debe usar un puntero o una referencia a std::byte
(a partir de C++ 17) si desea lograr la representación en bits de algunos datos; casi siempre es una operación legal. Otros tipos "seguros" son char
y unsigned char
, pero yo diría que no deberían usarse para ese propósito en C++ moderno ya que std::byte
tiene una mejor semántica.
Ejemplo: es cierto quereinterpret_cast
no es portátil por una razón: el orden de los bytes (endianidad). Pero, sorprendentemente, esta suele ser la mejor razón para utilizarlo. Imaginemos el ejemplo: tienes que leer un número binario de 32 bits de un archivo y sabes que es big endian. Su código debe ser genérico y debe funcionar correctamente en sistemas big endian (por ejemplo, algunos ARM) y little endian (por ejemplo, x86). Entonces debes verificar el orden de los bytes. Es bien conocido en tiempo de compilación por lo que puedes escribir Puedes escribir una función para lograr esto:constexpr
funciones:
/*constexpr*/ bool is_little_endian() {
std::uint16_t x=0x0001;
auto p = reinterpret_cast<std::uint8_t*>(&x);
return *p != 0;
}
Explicación: la representación binariax
en la memoria podría ser0000'0000'0000'0001
(grande) o0000'0001'0000'0000
(little endian). Después de reinterpretar la conversión, el byte bajop
el puntero podría ser respectivamente0000'0000
o0000'0001
. Si usa conversión estática, siempre será0000'0001
, sin importar qué endianidad se use.
EDITAR:
En la primera versión hice que la función de ejemplo is_little_endian
fuera constexpr
. Se compila bien en el gcc más nuevo (8.3.0), pero el estándar dice que es ilegal. El compilador clang se niega a compilarlo (lo cual es correcto).
El significado de reinterpret_cast
no está definido por el estándar C++. Por lo tanto, en teoría, un reinterpret_cast
programa podría bloquearse. En la práctica, los compiladores intentan hacer lo que usted espera, que es interpretar los bits de lo que está pasando como si fueran del tipo al que está transmitiendo. Si sabes qué hacen los compiladores que vas a utilizar reinterpret_cast
puedes usarlo, pero decir que es portátil sería mentir.
Para el caso que usted describe, y prácticamente cualquier caso en el que pueda considerar reinterpret_cast
, puede utilizar static_cast
alguna otra alternativa. Entre otras cosas, la norma dice lo siguiente sobre lo que se puede esperar static_cast
(§5.2.9):
Un rvalue de tipo "puntero a cv void" se puede convertir explícitamente en un puntero a tipo de objeto. Un valor de tipo puntero a objeto convertido a “puntero a cv void” y de nuevo al tipo de puntero original tendrá su valor original.
Entonces, para su caso de uso, parece bastante claro que el comité de estandarización tenía la intención de que usted usara static_cast
.