¿Cuándo se deben utilizar static_cast,dynamic_cast, const_cast y reinterpret_cast?
¿Cuáles son los usos adecuados de:
static_cast
dynamic_cast
const_cast
reinterpret_cast
(type)value
(elenco estilo C)type(value)
(reparto de estilo funcional)
¿Cómo se decide cuál utilizar en qué casos específicos?
static_cast
static_cast
es el primer yeso que debes intentar utilizar. Hace cosas como conversiones implícitas entre tipos (como int
a float
o puntero a void*
) y también puede llamar a funciones de conversión explícitas (o implícitas). En muchos casos, no es necesario indicarlo explícitamente static_cast
, pero es importante tener en cuenta que la T(something)
sintaxis es equivalente a (T)something
y debe evitarse (más sobre esto más adelante). T(something, something_else)
Sin embargo, A es seguro y está garantizado que llamará al constructor.
static_cast
También se puede transmitir a través de jerarquías de herencia. No es necesario cuando se lanza hacia arriba (hacia una clase base), pero cuando se lanza hacia abajo se puede usar siempre y cuando no se transmita por virtual
herencia. Sin embargo, no realiza comprobaciones y es un comportamiento indefinido bajar static_cast
una jerarquía a un tipo que en realidad no es el tipo del objeto.
const_cast
const_cast
se puede utilizar para eliminar o agregar const
una variable; ninguna otra versión de C++ es capaz de eliminarlo (ni siquiera reinterpret_cast
). Es importante tener en cuenta que modificar un const
valor anterior solo es indefinido si la variable original es const
; si lo usa para eliminar const
una referencia a algo que no se declaró con const
, es seguro. Esto puede resultar útil al sobrecargar funciones miembro basadas en const
, por ejemplo. También se puede usar para agregar const
a un objeto, como para llamar a una sobrecarga de función miembro.
const_cast
también funciona de manera similar volatile
, aunque es menos común.
dynamic_cast
dynamic_cast
se utiliza exclusivamente para manejar polimorfismo. Puede convertir un puntero o referencia a cualquier tipo polimórfico a cualquier otro tipo de clase (un tipo polimórfico tiene al menos una función virtual, declarada o heredada). Puedes usarlo para algo más que lanzar hacia abajo: puedes lanzar hacia un lado o incluso hacia arriba de otra cadena. Buscará dynamic_cast
el objeto deseado y lo devolverá si es posible. Si no puede, regresará nullptr
en el caso de un puntero o arrojará std::bad_cast
en el caso de una referencia.
dynamic_cast
Sin embargo, tiene algunas limitaciones. No funciona si hay varios objetos del mismo tipo en la jerarquía de herencia (el llamado "diamante temido") y no estás usando virtual
la herencia. Además, sólo puede pasar por herencia pública; siempre no pasará protected
por private
herencia. Sin embargo, esto rara vez es un problema, ya que este tipo de formas de herencia son raras.
reinterpret_cast
reinterpret_cast
Es el yeso más peligroso y debe usarse con mucha moderación. Convierte un tipo directamente en otro, como convertir el valor de un puntero a otro, o almacenar un puntero en un archivo int
, o todo tipo de otras cosas desagradables. En gran medida, la única garantía que obtiene reinterpret_cast
es que normalmente, si vuelve a convertir el resultado al tipo original, obtendrá exactamente el mismo valor (pero no si el tipo intermedio es más pequeño que el tipo original). Hay una serie de conversiones que reinterpret_cast
tampoco se pueden realizar. A menudo se abusa de él para conversiones y manipulaciones de bits particularmente extrañas, como convertir un flujo de datos sin procesar en datos reales o almacenar datos en los bits bajos de un puntero a datos alineados. Para esos casos, ver std::bit_cast
.
Elenco estilo C y elenco estilo función
La conversión estilo C y la conversión estilo función son conversiones que utilizan (type)object
o type(object)
, respectivamente, y son funcionalmente equivalentes. Se definen como el primero de los siguientes que tiene éxito:
const_cast
static_cast
(aunque ignorando las restricciones de acceso)static_cast
(ver arriba), entoncesconst_cast
reinterpret_cast
reinterpret_cast
, entoncesconst_cast
Por lo tanto, puede usarse como reemplazo de otras conversiones en algunos casos, pero puede ser extremadamente peligroso debido a la capacidad de transformarse en un reinterpret_cast
, y se debe preferir este último cuando se necesita una conversión explícita, a menos que esté seguro de static_cast
que tendrá éxito o reinterpret_cast
fracasará. . Incluso entonces, considere la opción más larga y explícita.
Las conversiones estilo C también ignoran el control de acceso cuando realizan una operación static_cast
, lo que significa que tienen la capacidad de realizar una operación que ninguna otra conversión puede realizar. Sin embargo, esto es principalmente una pifia y, en mi opinión, es solo otra razón para evitar los elencos de estilo C.
std::bit_cast
[C++20]
std::bit_cast
copia los bits y bytes del objeto de origen (su representación) directamente en un nuevo objeto del tipo de destino. Es una forma compatible con los estándares de hacer juegos de palabras tipográficos. Si te encuentras escribiendo *reinterpret_cast<SomeType*>(&x)
, probablemente deberías utilizarlo std::bit_cast<SomeType>(x)
en su lugar.
std::bit_cast
se declara en <bit>
. Los objetos deben tener el mismo tamaño y ser fácilmente copiables. Si aún no puede usar C++ 20, úselo memcpy
para copiar el valor fuente en una variable del tipo deseado.
Úselo
dynamic_cast
para convertir punteros/referencias dentro de una jerarquía de herencia.Úselo
static_cast
para conversiones de tipos normales.Úselo
reinterpret_cast
para la reinterpretación de bajo nivel de patrones de bits. Úselo con extrema precaución.Úselo
const_cast
para desecharconst/volatile
. Evite esto a menos que esté atrapado usando una API constante incorrecta.
(Arriba se han dado muchas explicaciones teóricas y conceptuales)
A continuación se muestran algunos de los ejemplos prácticos cuando utilicé static_cast , dynamic_cast , const_cast , reinterpret_cast .
(También consulte esto para comprender la explicación: http://www.cplusplus.com/doc/tutorial/typecasting/ )
transmisión_estática:
OnEventData(void* pData)
{
......
// pData is a void* pData,
// EventData is a structure e.g.
// typedef struct _EventData {
// std::string id;
// std:: string remote_id;
// } EventData;
// On Some Situation a void pointer *pData
// has been static_casted as
// EventData* pointer
EventData *evtdata = static_cast<EventData*>(pData);
.....
}
transmisión_dinámica:
void DebugLog::OnMessage(Message *msg)
{
static DebugMsgData *debug;
static XYZMsgData *xyz;
if(debug = dynamic_cast<DebugMsgData*>(msg->pdata)){
// debug message
}
else if(xyz = dynamic_cast<XYZMsgData*>(msg->pdata)){
// xyz message
}
else/* if( ... )*/{
// ...
}
}
const_cast:
// *Passwd declared as a const
const unsigned char *Passwd
// on some situation it require to remove its constness
const_cast<unsigned char*>(Passwd)
reinterpret_cast:
typedef unsigned short uint16;
// Read Bytes returns that 2 bytes got read.
bool ByteBuffer::ReadUInt16(uint16& val) {
return ReadBytes(reinterpret_cast<char*>(&val), 2);
}
Podría ser útil si conoces un poco los aspectos internos...
transmisión_estática
- El compilador de C++ ya sabe cómo convertir entre tipos de escalador
float
comoint
. Úselostatic_cast
para ellos. - Cuando le pide al compilador que convierta de tipo
A
aB
,static_cast
llamaB
al constructor pasándoloA
como parámetro. Alternativamente,A
podría tener un operador de conversión (es decirA::operator B()
). SiB
no tiene dicho constructor, oA
no tiene un operador de conversión, obtendrá un error en tiempo de compilación. - La transmisión desde
A*
aB*
siempre se realiza correctamente si A y B están en la jerarquía de herencia (o son nulos); de lo contrario, obtendrá un error de compilación. - Entendido : si convierte el puntero base en un puntero derivado pero si el objeto real no es realmente un tipo derivado, entonces no obtendrá ningún error. Obtiene un puntero incorrecto y muy probablemente un error de segmentación en tiempo de ejecución. Lo mismo ocurre
A&
conB&
. - Gotcha : ¡Transmitir desde Derivado a Base o viceversa crea una nueva copia! Para las personas que vienen de C#/Java, esto puede ser una gran sorpresa porque el resultado es básicamente un objeto cortado creado a partir de Derived.
transmisión_dinámica
- Dynamic_cast utiliza información del tipo de tiempo de ejecución para determinar si la conversión es válida. Por ejemplo,
(Base*)
puede(Derived*)
fallar si el puntero no es realmente de tipo derivado. - Esto significa que Dynamic_cast es muy caro en comparación con static_cast.
- Para
A*
toB*
, si la conversión no es válida, dinámica_cast devolverá nullptr. - Porque si la conversión no es válida
A&
,B&
Dynamic_cast arrojará una excepción bad_cast. - A diferencia de otras conversiones, hay una sobrecarga de tiempo de ejecución.
const_cast
- Si bien static_cast puede funcionar de manera no constante a constante, no puede ser al revés. El const_cast puede funcionar en ambos sentidos.
- Un ejemplo en el que esto resulta útil es iterar a través de algún contenedor
set<T>
que solo devuelve sus elementos como constantes para asegurarse de no cambiar su clave. Sin embargo, si su intención es modificar los miembros que no son clave del objeto, entonces debería estar bien. Puedes usar const_cast para eliminar la constancia. - Otro ejemplo es cuando desea implementar
T& SomeClass::foo()
además deconst T& SomeClass::foo() const
. Para evitar la duplicación de código, puede aplicar const_cast para devolver el valor de una función de otra.
reinterpretar_cast
- Básicamente, esto dice que tome estos bytes en esta ubicación de memoria y considérelo como un objeto determinado.
- Por ejemplo, puede cargar de 4 bytes
float
a 4 bytes deint
para ver cómofloat
se ven los bits. - Obviamente, si los datos no son correctos para el tipo, es posible que se produzca un error de segmentación.
- No hay sobrecarga de tiempo de ejecución para esta transmisión.
static_cast
vs dynamic_cast
vs reinterpret_cast
vista interna en un abatido/upcast
En esta respuesta, quiero comparar estos tres mecanismos en un ejemplo concreto de upcast/downcast y analizar qué sucede con los punteros/memoria/ensamblado subyacentes para brindar una comprensión concreta de cómo se comparan.
Creo que esto dará una buena intuición sobre en qué se diferencian esos elencos:
static_cast
: realiza una compensación de dirección en tiempo de ejecución (bajo impacto en tiempo de ejecución) y no realiza comprobaciones de seguridad de que una conversión abatida sea correcta.dyanamic_cast
: realiza la misma compensación de dirección en tiempo de ejecución comostatic_cast
, pero también realiza una costosa verificación de seguridad de que una conversión abatida es correcta usando RTTI.Esta verificación de seguridad le permite consultar si un puntero de clase base es de un tipo determinado en tiempo de ejecución al verificar un retorno que
nullptr
indica una conversión no válida.Por lo tanto, si su código no puede verificar eso
nullptr
y realizar una acción válida que no sea de cancelación, simplemente debe usarstatic_cast
en lugar de la conversión dinámica.Si cancelar es la única acción que su código puede realizar, tal vez solo desee habilitar las
dynamic_cast
compilaciones de depuración (-NDEBUG
) y usarlasstatic_cast
de otra manera, por ejemplo, como se hace aquí , para no ralentizar sus ejecuciones rápidas.reinterpret_cast
: no hace nada en tiempo de ejecución, ni siquiera el desplazamiento de dirección. El puntero debe apuntar exactamente al tipo correcto, ni siquiera una clase base funciona. Por lo general, no desea esto a menos que se trate de flujos de bytes sin formato.
Considere el siguiente ejemplo de código:
principal.cpp
#include <iostream>
struct B1 {
B1(int int_in_b1) : int_in_b1(int_in_b1) {}
virtual ~B1() {}
void f0() {}
virtual int f1() { return 1; }
int int_in_b1;
};
struct B2 {
B2(int int_in_b2) : int_in_b2(int_in_b2) {}
virtual ~B2() {}
virtual int f2() { return 2; }
int int_in_b2;
};
struct D : public B1, public B2 {
D(int int_in_b1, int int_in_b2, int int_in_d)
: B1(int_in_b1), B2(int_in_b2), int_in_d(int_in_d) {}
void d() {}
int f2() { return 3; }
int int_in_d;
};
int main() {
B2 *b2s[2];
B2 b2{11};
D *dp;
D d{1, 2, 3};
// The memory layout must support the virtual method call use case.
b2s[0] = &b2;
// An upcast is an implicit static_cast<>().
b2s[1] = &d;
std::cout << "&d " << &d << std::endl;
std::cout << "b2s[0] " << b2s[0] << std::endl;
std::cout << "b2s[1] " << b2s[1] << std::endl;
std::cout << "b2s[0]->f2() " << b2s[0]->f2() << std::endl;
std::cout << "b2s[1]->f2() " << b2s[1]->f2() << std::endl;
// Now for some downcasts.
// Cannot be done implicitly
// error: invalid conversion from ‘B2*’ to ‘D*’ [-fpermissive]
// dp = (b2s[0]);
// Undefined behaviour to an unrelated memory address because this is a B2, not D.
dp = static_cast<D*>(b2s[0]);
std::cout << "static_cast<D*>(b2s[0]) " << dp << std::endl;
std::cout << "static_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;
// OK
dp = static_cast<D*>(b2s[1]);
std::cout << "static_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "static_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
// Segfault because dp is nullptr.
dp = dynamic_cast<D*>(b2s[0]);
std::cout << "dynamic_cast<D*>(b2s[0]) " << dp << std::endl;
//std::cout << "dynamic_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;
// OK
dp = dynamic_cast<D*>(b2s[1]);
std::cout << "dynamic_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "dynamic_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
// Undefined behaviour to an unrelated memory address because this
// did not calculate the offset to get from B2* to D*.
dp = reinterpret_cast<D*>(b2s[1]);
std::cout << "reinterpret_cast<D*>(b2s[1]) " << dp << std::endl;
std::cout << "reinterpret_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
}
Compile, ejecute y desmonte con:
g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
setarch `uname -m` -R ./main.out
gdb -batch -ex "disassemble/rs main" main.out
donde setarch
se utiliza para desactivar ASLR y facilitar la comparación de ejecuciones.
Posible salida:
&d 0x7fffffffc930
b2s[0] 0x7fffffffc920
b2s[1] 0x7fffffffc940
b2s[0]->f2() 2
b2s[1]->f2() 3
static_cast<D*>(b2s[0]) 0x7fffffffc910
static_cast<D*>(b2s[0])->int_in_d 1
static_cast<D*>(b2s[1]) 0x7fffffffc930
static_cast<D*>(b2s[1])->int_in_d 3
dynamic_cast<D*>(b2s[0]) 0
dynamic_cast<D*>(b2s[1]) 0x7fffffffc930
dynamic_cast<D*>(b2s[1])->int_in_d 3
reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940
reinterpret_cast<D*>(b2s[1])->int_in_d 32767
Ahora, como se menciona en: https://en.wikipedia.org/wiki/Virtual_method_table para soportar las llamadas a métodos virtuales de manera eficiente, suponiendo que las estructuras de datos de memoria de B1 tienen la forma:
B1:
+0: pointer to virtual method table of B1
+4: value of int_in_b1
y B2
es de forma:
B2:
+0: pointer to virtual method table of B2
+4: value of int_in_b2
entonces la estructura de datos de la memoria D
debe verse así:
D:
+0: pointer to virtual method table of D (for B1)
+4: value of int_in_b1
+8: pointer to virtual method table of D (for B2)
+12: value of int_in_b2
+16: value of int_in_d
El hecho clave es que la estructura de datos de la memoria de D
contiene en su interior una estructura de memoria idéntica a la de B1
y B2
, es decir:
- +0 se ve exactamente como B1, con la tabla virtual B1 para D seguida de
int_in_b1
- +8 se ve exactamente como un B2, con la tabla B2 para D seguida de
int_in_b2
o en un nivel superior:
D:
+0: B1
+8: B2
+16: <fields of D itsef>
Por lo tanto llegamos a la conclusión crítica:
un upcast o downcast solo necesita cambiar el valor del puntero por un valor conocido en el momento de la compilación
De esta manera, cuando D
se pasa a la matriz de tipo base, la conversión de tipos realmente calcula ese desplazamiento y señala algo que se ve exactamente como válido B2
en la memoria, excepto que este tiene el vtable en D
lugar de B2
y, por lo tanto, todas las llamadas virtuales funcionan de forma transparente.
P.ej:
b2s[1] = &d;
simplemente necesita obtener la dirección de d
+ 8 para llegar a la estructura de datos similar a B2 correspondiente.
Ahora finalmente podemos volver a la conversión de tipos y al análisis de nuestro ejemplo concreto.
Desde la salida estándar vemos:
&d 0x7fffffffc930
b2s[1] 0x7fffffffc940
Por lo tanto, lo implícito static_cast
realizado allí calculó correctamente el desplazamiento de la D
estructura de datos completa en 0x7fffffffc930 a la B2
similar que está en 0x7fffffffc940. También inferimos que lo que se encuentra entre 0x7ffffffc930 y 0x7fffffffc940 probablemente sean los B1
datos y la tabla virtual.
Luego, en las secciones abatidas, ahora es fácil entender cómo fallan las no válidas y por qué:
static_cast<D*>(b2s[0]) 0x7fffffffc910
: el compilador simplemente subió 0x10 en bytes en tiempo de compilación para intentar ir de aB2
a lo que contieneD
Pero como
b2s[0]
no era unD
, ahora apunta a una región de memoria indefinida.El desmontaje es:
49 dp = static_cast<D*>(b2s[0]); 0x0000000000000fc8 <+414>: 48 8b 45 d0 mov -0x30(%rbp),%rax 0x0000000000000fcc <+418>: 48 85 c0 test %rax,%rax 0x0000000000000fcf <+421>: 74 0a je 0xfdb <main()+433> 0x0000000000000fd1 <+423>: 48 8b 45 d0 mov -0x30(%rbp),%rax 0x0000000000000fd5 <+427>: 48 83 e8 10 sub $0x10,%rax 0x0000000000000fd9 <+431>: eb 05 jmp 0xfe0 <main()+438> 0x0000000000000fdb <+433>: b8 00 00 00 00 mov $0x0,%eax 0x0000000000000fe0 <+438>: 48 89 45 98 mov %rax,-0x68(%rbp)
entonces vemos que GCC hace:
- compruebe si el puntero es NULL y, en caso afirmativo, devuelva NULL
- de lo contrario, reste 0x10 para llegar al
D
que no existe
dynamic_cast<D*>(b2s[0]) 0
: ¡C ++ realmente descubrió que la conversión no era válida y la devolviónullptr
!No hay forma de que esto se pueda hacer en tiempo de compilación, y lo confirmaremos desde el desmontaje:
59 dp = dynamic_cast<D*>(b2s[0]); 0x00000000000010ec <+706>: 48 8b 45 d0 mov -0x30(%rbp),%rax 0x00000000000010f0 <+710>: 48 85 c0 test %rax,%rax 0x00000000000010f3 <+713>: 74 1d je 0x1112 <main()+744> 0x00000000000010f5 <+715>: b9 10 00 00 00 mov $0x10,%ecx 0x00000000000010fa <+720>: 48 8d 15 f7 0b 20 00 lea 0x200bf7(%rip),%rdx # 0x201cf8 <_ZTI1D> 0x0000000000001101 <+727>: 48 8d 35 28 0c 20 00 lea 0x200c28(%rip),%rsi # 0x201d30 <_ZTI2B2> 0x0000000000001108 <+734>: 48 89 c7 mov %rax,%rdi 0x000000000000110b <+737>: e8 c0 fb ff ff callq 0xcd0 <__dynamic_cast@plt> 0x0000000000001110 <+742>: eb 05 jmp 0x1117 <main()+749> 0x0000000000001112 <+744>: b8 00 00 00 00 mov $0x0,%eax 0x0000000000001117 <+749>: 48 89 45 98 mov %rax,-0x68(%rbp)
Primero hay una verificación NULL y devuelve NULL si la entrada es NULL.
De lo contrario, configura algunos argumentos en RDX, RSI y RDI y llama
__dynamic_cast
.No tengo la paciencia para analizar esto más a fondo ahora, pero como dijeron otros, la única forma de que esto funcione es
__dynamic_cast
acceder a algunas estructuras de datos RTTI en memoria adicionales que representan la jerarquía de clases.Por lo tanto, debe comenzar desde la
B2
entrada de esa tabla y luego recorrer esta jerarquía de clases hasta encontrar que la vtable para unD
encasillado deb2s[0]
.¡Es por eso que el reparto dinámico es potencialmente costoso! A continuación se muestra un ejemplo en el que un parche de una sola línea que convierte a
dynamic_cast
en astatic_cast
en un proyecto complejo redujo el tiempo de ejecución en un 33 %. .reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940
éste simplemente nos cree ciegamente: dijimos que hay unaD
dirección atb2s[1]
y el compilador no realiza cálculos de compensación.Pero esto está mal, porque D está en realidad en 0x7fffffffc930, ¡lo que está en 0x7fffffffc940 es la estructura similar a B2 dentro de D! Entonces se accede a la basura.
Podemos confirmar esto a partir del horrendo
-O0
ensamblaje que simplemente mueve el valor:70 dp = reinterpret_cast<D*>(b2s[1]); 0x00000000000011fa <+976>: 48 8b 45 d8 mov -0x28(%rbp),%rax 0x00000000000011fe <+980>: 48 89 45 98 mov %rax,-0x68(%rbp)
Preguntas relacionadas:
- ¿Cómo se implementa Dynamic_cast?
- Downcasting usando 'static_cast' en C++
Probado en Ubuntu 18.04 amd64, GCC 7.4.0.