¿Cuándo deben usarse static_cast, dynamic_cast, const_cast y reinterpret_cast?


2496

¿Cuáles son los usos adecuados de:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Reparto estilo C (type)value
  • Elenco de estilo funcional type(value)

¿Cómo se decide cuál usar en qué casos específicos?



3
Para ver algunos ejemplos concretos útiles del uso de diferentes tipos de modelos, puede consultar la primera respuesta a una pregunta similar en este otro tema .
TeaMonkie

2
Puede encontrar respuestas realmente buenas para su pregunta anterior. Pero me gustaría poner un punto más aquí, @ e.James "No hay nada que puedan hacer estos nuevos operadores de conversión de c ++ y que la conversión de estilo de c no pueda. Se agregan más o menos para una mejor legibilidad del código".
BreakBadSP

@BreakBadSP Los nuevos lanzamientos no son solo para una mejor legibilidad del código. Están allí para hacer que sea más difícil hacer cosas peligrosas, como desechar los const o los punteros en lugar de sus valores. ¡static_cast tiene muchas menos posibilidades de hacer algo peligroso que el reparto de estilo CA!
Cuarenta

@FourtyTwo estuvo de acuerdo
BreakBadSP

Respuestas:


2571

static_castes el primer lanzamiento que debes intentar usar. Hace cosas como conversiones implícitas entre tipos (como intto floato pointer to void*), y también puede llamar a funciones de conversión explícitas (o implícitas). En muchos casos, static_castno es necesario declarar explícitamente , pero es importante tener en cuenta que la T(something)sintaxis es equivalente (T)somethingy debe evitarse (más sobre eso más adelante). T(something, something_else)Sin embargo, A es seguro y está garantizado para llamar al constructor.

static_castTambién puede emitir 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 que no se lance por virtualherencia. Sin embargo, no realiza comprobaciones y es un comportamiento indefinido static_castreducir una jerarquía a un tipo que en realidad no es el tipo del objeto.


const_castse puede usar para eliminar o agregar consta una variable; ningún otro elenco de C ++ es capaz de eliminarlo (ni siquiera reinterpret_cast). Es importante tener en cuenta que modificar un constvalor anterior solo no está definido si la variable original es const; si lo usa para quitar constuna referencia a algo con lo que no se declaró const, es seguro. Esto puede ser útil al sobrecargar funciones de miembros basadas const, por ejemplo. También se puede usar para agregar consta un objeto, como llamar a una sobrecarga de la función miembro.

const_castTambién funciona de manera similar volatile, aunque eso es menos común.


dynamic_castSe utiliza exclusivamente para el manejo del polimorfismo. Puede lanzar 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). Puede usarlo para algo más que lanzar hacia abajo: puede lanzar hacia los lados o incluso hacia arriba en otra cadena. El dynamic_castbuscará el objeto deseado y devolverlo si es posible. Si no puede, regresará nullptren el caso de un puntero, o arrojará std::bad_casten el caso de una referencia.

dynamic_castSin 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á utilizando la virtualherencia. También solo puede pasar por herencia pública: siempre fallará en viajar protectedo en privateherencia. Sin embargo, esto rara vez es un problema, ya que tales formas de herencia son raras.


reinterpret_castes el yeso más peligroso y debe usarse con moderación. Convierte un tipo directamente en otro, como convertir el valor de un puntero a otro, o almacenar un puntero en una into toda otra clase de cosas desagradables. En gran medida, la única garantía con la que obtiene reinterpret_castes que normalmente si devuelve 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). También hay varias conversiones que reinterpret_castno pueden hacer. Se utiliza principalmente para conversiones particularmente extrañas y manipulaciones de bits, como convertir un flujo de datos sin procesar en datos reales o almacenar datos en los bits bajos de un puntero en datos alineados.


La conversión de estilo C y la conversión de estilo funcional son conversiones utilizando (type)objecto 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), entonces const_cast
  • reinterpret_cast
  • reinterpret_cast, entonces const_cast

Por lo tanto, puede usarse como reemplazo de otros lanzamientos en algunos casos, pero puede ser extremadamente peligroso debido a la capacidad de convertirse en a reinterpret_cast, y este último debe preferirse cuando se necesita un lanzamiento explícito, a menos que esté seguro de que static_casttendrá éxito o reinterpret_castfallará . Incluso entonces, considere la opción más larga y más explícita.

Los lanzamientos de estilo C también ignoran el control de acceso al realizar un static_cast, lo que significa que tienen la capacidad de realizar una operación que ningún otro lanzamiento puede. Sin embargo, esto es principalmente un error, y en mi opinión es solo otra razón para evitar los moldes de estilo C.


17
dynamic_cast es solo para tipos polimórficos. solo necesita usarlo cuando está enviando a una clase derivada. static_cast es ciertamente la primera opción a menos que necesite específicamente la funcionalidad de dynamic_cast. En general, no se trata de un milagroso "elenco de verificación de tipo" de bala de plata.
jalf

2
¡Gran respuesta! Una observación rápida: static_cast podría ser necesario para generar la jerarquía en caso de que tenga un Derivado * y para convertirlo en Base * y, ya que los punteros / referencias dobles no automáticamente proyectan la jerarquía. Me encontré con esa situación (francamente, no común) hace dos minutos. ;-)
bartgol

55
* "ningún otro elenco de C ++ es capaz de eliminar const(ni siquiera reinterpret_cast)" ... ¿en serio? ¿Qué hay de reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
user541686

29
Creo que un detalle importante que falta arriba es que dynamic_cast tiene una penalización de rendimiento en tiempo de ejecución en comparación con static o reinterpret_cast. Esto es importante, por ejemplo, en software en tiempo real.
jfritz42

55
Vale la pena mencionar que a reinterpret_castmenudo es el arma de elección cuando se trata con un conjunto de tipos de datos opacos de una API
Class Skeleton

333

Se usa dynamic_castpara convertir punteros / referencias dentro de una jerarquía de herencia.

Úselo static_castpara conversiones de tipo ordinario.

Úselo reinterpret_castpara la reinterpretación de bajo nivel de patrones de bits. Usar con extrema precaución.

Úselo const_castpara desechar const/volatile. Evita esto a menos que estés atascado usando una API const-incorrecta


2
Tenga cuidado con dynamic_cast. Se basa en RTTI y esto no funcionará como se esperaba en los límites de las bibliotecas compartidas. Simplemente porque crea una biblioteca ejecutable y compartida de forma independiente, no hay una forma estandarizada de sincronizar RTTI en diferentes compilaciones. Por esta razón, en la biblioteca Qt existe qobject_cast <> que usa la información de tipo QObject para verificar los tipos.
user3150128

198

(Se ha dado mucha explicación teórica y conceptual arriba)

A continuación se presentan algunos ejemplos prácticos cuando utilicé static_cast , dynamic_cast , const_cast , reinterpret_cast .

(También hace referencia a esto para comprender la explicación: http://www.cplusplus.com/doc/tutorial/typecasting/ )

static_cast:

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);
  .....
}

Dynamic_cast:

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);
}

31
La teoría de algunas de las otras respuestas es buena, pero aún confusa, ver estos ejemplos después de leer las otras respuestas realmente hace que todos tengan sentido. Sin los ejemplos, todavía no estaba seguro, pero con ellos, ahora estoy seguro de lo que significan las otras respuestas.
Solx

1
Sobre el último uso de reinterpret_cast: ¿no es lo mismo que usar static_cast<char*>(&val)?
Lorenzo Belli

3
@LorenzoBelli Por supuesto que no. ¿Lo intentaste? Este último no es válido en C ++ y bloquea la compilación. static_castsolo funciona entre tipos con conversiones definidas, relación visible por herencia o hacia / desde void *. Para todo lo demás, hay otros moldes. reinterpret casta cualquier char *tipo se le permite leer la representación de cualquier objeto, y uno de los únicos casos en que esa palabra clave es útil, no un generador desenfrenado de implementación / comportamiento indefinido. Pero esto no se considera una conversión 'normal', por lo que no está permitido por los (generalmente) muy conservadores static_cast.
underscore_d

2
reinterpret_cast es bastante común cuando trabaja con software de sistema como bases de datos. En la mayoría de los casos, escribe su propio administrador de página que no tiene idea de cuál es el tipo de datos almacenado en la página y simplemente devuelve un puntero vacío. Depende de los niveles más altos hacer un reparto reinterpretado e inferirlo como quieran.
Sohaib

1
El ejemplo const_cast exhibe Comportamiento indefinido. Una variable declarada como const no puede ser desconst-ed. Sin embargo, una variable declarada como no constante que se pasa a una función que toma una referencia constante puede en esa función ser desconsolidada sin ser UB.
Johann Gerell el

99

Podría ayudar si conoces un poco de aspectos internos ...

static_cast

  • El compilador de C ++ ya sabe cómo convertir entre tipos de escaladores como float a int. Úselo static_castpara ellos.
  • Cuando le pide al compilador que convierta de tipo Aa B, el constructor de static_castllamadas Bpasa Acomo parámetro. Alternativamente, Apodría tener un operador de conversión (es decir A::operator B()). Si Bno tiene dicho constructor, o Ano tiene un operador de conversión, entonces obtiene un error de tiempo de compilación.
  • La conversión de A*a B*siempre tiene éxito si A y B están en la jerarquía de herencia (o nula), de lo contrario, obtendrá un error de compilación.
  • Gotcha : si lanzas el puntero base al puntero derivado pero si el objeto real no es realmente un tipo derivado, entonces no obtienes un error. Obtiene un mal puntero y muy probablemente un segfault en tiempo de ejecución. Lo mismo va para A&a B&.
  • Gotcha : Cast from Derived to 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.

Dynamic_cast

  • dynamic_cast utiliza información de tipo de tiempo de ejecución para determinar si la conversión es válida. Por ejemplo, (Base*)a (Derived*)puede fallar si el puntero no es realmente de tipo derivado.
  • Esto significa que dynamic_cast es muy costoso en comparación con static_cast!
  • Para A*que B*, si es fundido moldeado dinámico no válido a continuación volverá nullptr.
  • Para A&a B&si la conversión no es válida, dynamic_cast generará una excepción bad_cast.
  • A diferencia de otros moldes, hay tiempo de ejecución por encima.

const_cast

  • Si bien static_cast puede hacer no const a const, no puede ser al revés. Const_cast puede hacer ambas cosas.
  • Un ejemplo donde esto es útil es iterar a través de algún contenedor como el set<T>que solo devuelve sus elementos como constante para asegurarse de que no cambie su clave. Sin embargo, si su intención es modificar los miembros no clave del objeto, entonces debería estar bien. Puede usar const_cast para eliminar la constness.
  • Otro ejemplo es cuando desea implementar T& SomeClass::foo()tan bien como const 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.

reinterpret_cast

  • Básicamente, esto dice que tome estos bytes en esta ubicación de memoria y piense en él como un objeto dado.
  • Por ejemplo, puede cargar 4 bytes de flotante a 4 bytes de int para ver cómo se ven los bits en flotante.
  • Obviamente, si los datos no son correctos para el tipo, puede obtener segfault.
  • No hay tiempo de ejecución por encima de este elenco.

Agregué la información del operador de conversión, pero hay algunas otras cosas que también deberían arreglarse y no me siento cómodo actualizando demasiado esto. Los elementos son: 1. If you cast base pointer to derived pointer but if actual object is not really derived type then you don't get error. You get bad pointer and segfault at runtime.Obtiene UB que puede resultar en una falla predeterminada en el tiempo de ejecución si tiene suerte. 2. Los lanzamientos dinámicos también se pueden usar en el lanzamiento cruzado. 3. Los moldes de Const pueden dar lugar a UB en algunos casos. Usar mutablepuede ser una mejor opción para implementar la constidad lógica.
Adrian

1
@ Adrian, tienes razón en todos los aspectos. La respuesta está escrita para personas en un nivel más o menos principiante y no quise abrumarlos con todas las demás complicaciones que mutable
conlleva

16

¿ Responde esto a tu pregunta?

Nunca lo he usado reinterpret_cast, y me pregunto si encontrarse con un caso que lo necesita no es un olor a mal diseño. En la base de código en la que trabajo dynamic_castse usa mucho. La diferencia con static_castes que una dynamic_castcomprobación de tiempo de ejecución puede ser (más segura) o no (más sobrecarga) lo que desea (consulte msdn ).


3
He usado reintrepret_cast para un propósito: obtener los bits de un doble (el mismo tamaño que el largo en mi plataforma).
Joshua el

2
reinterpret_cast es necesario, por ejemplo, para trabajar con objetos COM. CoCreateInstance () tiene un parámetro de salida de tipo void ** (el último parámetro), en el que pasará su puntero declarado como, por ejemplo, "INetFwPolicy2 * pNetFwPolicy2". Para hacer eso, debe escribir algo como reinterpret_cast <void **> (& pNetFwPolicy2).
Serge Rogatch

1
Quizás haya un enfoque diferente, pero lo uso reinterpret_castpara extraer datos de una matriz. Por ejemplo, si tengo un char*búfer grande lleno de datos binarios empaquetados que necesito mover y obtener primitivas individuales de diferentes tipos. Algo así:template<class ValType> unsigned int readValFromAddress(char* addr, ValType& val) { /*On platforms other than x86(_64) this could do unaligned reads, which could be bad*/ val = (*(reinterpret_cast<ValType*>(addr))); return sizeof(ValType); }
James Matta el

Nunca lo he usado reinterpret_cast, no hay muchos usos para ello.
Pika el mago de las ballenas

Personalmente, solo lo he visto reinterpret_castusado por una razón. He visto datos de objetos en bruto almacenados en un tipo de datos "blob" en una base de datos, luego, cuando los datos se recuperan de la base de datos, reinterpret_castse utilizan para convertir estos datos en bruto en el objeto.
ImaginaryHuman072889

15

Además de las otras respuestas hasta ahora, aquí hay un ejemplo obvio en el static_castque no es suficiente para que reinterpret_castsea ​​necesario. Supongamos que hay una función que en un parámetro de salida devuelve punteros a objetos de diferentes clases (que no comparten una clase base común). Un ejemplo real de dicha función es CoCreateInstance()(ver el último parámetro, que de hecho es void**). Suponga que solicita una clase particular de objeto de esta función, de modo que sepa de antemano el tipo del puntero (lo que suele hacer con los objetos COM). En este caso, no puede convertir el puntero en su puntero void**con static_cast: necesita reinterpret_cast<void**>(&yourPointer).

En codigo:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    //static_cast<void**>(&pNetFwPolicy2) would give a compile error
    reinterpret_cast<void**>(&pNetFwPolicy2) );

Sin embargo, static_castfunciona para punteros simples (no punteros a punteros), por lo que el código anterior se puede reescribir para evitar reinterpret_cast(al precio de una variable adicional) de la siguiente manera:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
void* tmp = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    &tmp );
pNetFwPolicy2 = static_cast<INetFwPolicy2*>(tmp);

¿No funcionaría algo así en &static_cast<void*>(pNetFwPolicy2)lugar de static_cast<void**>(&pNetFwPolicy2)?
jp48

9

Si bien otras respuestas describieron muy bien todas las diferencias entre los moldes de C ++, me gustaría agregar una breve nota de por qué no debería usar moldes de estilo C (Type) vary Type(var).

Para los principiantes de C ++, los lanzamientos de estilo C parecen ser la operación de superconjunto sobre los lanzamientos de C ++ (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()) y alguien podría preferirlos sobre los lanzamientos de C ++ . De hecho, el elenco de estilo C es el superconjunto y más corto de escribir.

El principal problema de los elencos estilo C es que ocultan la verdadera intención del desarrollador del elenco. Los lanzamientos de estilo C pueden realizar prácticamente todos los tipos de lanzamientos desde lanzamientos normalmente seguros realizados por static_cast <> () y dynamic_cast <> () a lanzamientos potencialmente peligrosos como const_cast <> (), donde el modificador const puede eliminarse para que las variables const se puede modificar y reinterpretar_cast <> () que incluso puede reinterpretar valores enteros en punteros.

Aquí está la muestra.

int a=rand(); // Random number.

int* pa1=reinterpret_cast<int*>(a); // OK. Here developer clearly expressed he wanted to do this potentially dangerous operation.

int* pa2=static_cast<int*>(a); // Compiler error.
int* pa3=dynamic_cast<int*>(a); // Compiler error.

int* pa4=(int*) a; // OK. C-style cast can do such cast. The question is if it was intentional or developer just did some typo.

*pa4=5; // Program crashes.

La razón principal por la que se agregaron los lanzamientos de C ++ al lenguaje fue para permitir que un desarrollador aclare sus intenciones, por qué va a hacer ese lanzamiento. Al usar modelos de estilo C que son perfectamente válidos en C ++, está haciendo que su código sea menos legible y más propenso a errores, especialmente para otros desarrolladores que no crearon su código. Por lo tanto, para que su código sea más legible y explícito, siempre debe preferir los moldes de C ++ sobre los moldes de estilo C.

Aquí hay una breve cita del libro de Bjarne Stroustrup (autor de C ++) The C ++ Programming Language 4th edition - página 302.

Este reparto de estilo C es mucho más peligroso que los operadores de conversión nombrados porque la notación es más difícil de detectar en un programa grande y el tipo de conversión previsto por el programador no es explícito.


5

Para comprender, consideremos el fragmento de código a continuación:

struct Foo{};
struct Bar{};

int main(int argc, char** argv)
{
    Foo* f = new Foo;

    Bar* b1 = f;                              // (1)
    Bar* b2 = static_cast<Bar*>(f);           // (2)
    Bar* b3 = dynamic_cast<Bar*>(f);          // (3)
    Bar* b4 = reinterpret_cast<Bar*>(f);      // (4)
    Bar* b5 = const_cast<Bar*>(f);            // (5)

    return 0;
}

Solo la línea (4) compila sin error. Solo reinterpret_cast puede usarse para convertir un puntero a un objeto a un puntero a cualquier tipo de objeto no relacionado.

Algo que debe tenerse en cuenta es que: Dynamic_cast fallará en tiempo de ejecución, sin embargo, en la mayoría de los compiladores también fallará al compilar porque no hay funciones virtuales en la estructura del puntero que se está lanzando , lo que significa que dynamic_cast funcionará solo con punteros de clase polimórficos .

Cuándo usar C ++ cast :

  • Utilice static_cast como el equivalente de una conversión de estilo C que realiza la conversión de valor, o cuando necesitemos lanzar explícitamente un puntero desde una clase a su superclase.
  • Use const_cast para eliminar el calificador const.
  • Utilice reinterpret_cast para realizar conversiones inseguras de tipos de puntero hacia y desde enteros y otros tipos de puntero. Use esto solo si sabemos lo que estamos haciendo y entendemos los problemas de alias.

3

static_castvs dynamic_castvs reinterpret_castinternos ver en un downcast / upcast

En esta respuesta, quiero comparar estos tres mecanismos en un ejemplo concreto de upcast / downcast y analizar qué sucede con los punteros subyacentes / memoria / ensamblaje para dar una comprensión concreta de cómo se comparan.

Creo que esto dará una buena intuición sobre cómo esos moldes son diferentes:

  • static_cast: hace un desplazamiento de dirección en tiempo de ejecución (bajo impacto de tiempo de ejecución) y no verifica la seguridad de que una descarga sea correcta.

  • dyanamic_cast: hace el mismo desplazamiento de dirección en tiempo de ejecución static_cast, pero también y una costosa verificación de seguridad de que un downcast es correcto usando RTTI.

    Esta comprobación de seguridad le permite consultar si un puntero de clase base es de un tipo dado en tiempo de ejecución al verificar una devolución de la nullptrcual indica un downcast no válido.

    Por lo tanto, si su código no puede verificar eso nullptry tomar una acción válida de no aborto, solo debe usarlo en static_castlugar de la conversión dinámica.

    Si un aborto es la única acción que puede tomar su código, tal vez solo desee habilitar las dynamic_castcompilaciones en depuración ( -NDEBUG), y usar lo static_castcontrario, 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 la 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 estén involucradas secuencias de bytes sin procesar.

Considere el siguiente ejemplo de código:

main.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;
}

Compila, ejecuta y desmonta 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 setarchse usa para deshabilitar ASLR para que sea más fácil comparar ejecuciones.

Salida posible:

&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 admitir las llamadas de métodos virtuales de manera eficiente, la estructura de datos de la memoria Dtiene que ser algo así como:

B1:
  +0: pointer to virtual method table of B1
  +4: value of int_in_b1

B2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

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 memoria Dcontiene dentro de ella una estructura de memoria compatible con la de B1y la de B2internamente.

Por lo tanto, llegamos a la conclusión crítica:

una subida o bajada solo necesita cambiar el valor del puntero por un valor conocido en tiempo de compilación

De esta manera, cuando Dse pasa a la matriz de tipos base, el tipo de conversión calcula ese desplazamiento y señala algo que se ve exactamente como válido B2en la memoria:

b2s[1] = &d;

excepto que este tiene la vtable para en Dlugar de B2, y por lo tanto todas las llamadas virtuales funcionan de manera transparente.

Ahora, finalmente podemos volver al tipo de fundición y al análisis de nuestro ejemplo concreto.

De la salida stdout vemos:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

Por lo tanto, lo implícito static_casthecho allí calculó correctamente el desplazamiento de la Destructura de datos completa en 0x7fffffffc930 a la B2similar que está en 0x7fffffffc940. También inferimos que lo que se encuentra entre 0x7fffffffc930 y 0x7fffffffc940 es probable que sean los B1datos y vtable.

Luego, en las secciones abatidas, ahora es fácil entender cómo fallan los no válidos y por qué:

  • static_cast<D*>(b2s[0]) 0x7fffffffc910: el compilador subió 0x10 en tiempo de compilación de bytes para intentar ir de un B2aD

    Pero debido a que b2s[0]no era un D, 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 a lo Dque no existe
  • dynamic_cast<D*>(b2s[0]) 0: ¡C ++ descubrió que el reparto no era válido y se 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 comprobación NULL, y devuelve NULL si la entrada es NULL.

    De lo contrario, establece algunos argumentos en RDX, RSI y RDI y las llamadas __dynamic_cast.

    No tengo la paciencia para analizar esto más a fondo ahora, pero como otros dijeron, la única forma de que esto funcione es __dynamic_castacceder a algunas estructuras de datos en memoria RTTI adicionales que representan la jerarquía de clases.

    Por lo tanto, debe comenzar desde la B2entrada para esa tabla, luego recorrer esta jerarquía de clases hasta que encuentre que la tabla vtable para un Dtypecast b2s[0].

    ¡Es por eso que reinterpretar el elenco es potencialmente costoso! ¡Aquí hay un ejemplo en el que un parche de una línea que convierte dynamic_casta a static_casten un proyecto complejo reduce el tiempo de ejecución en un 33%! .

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940este simplemente nos cree ciegamente: dijimos que hay una Ddirección at b2s[1], y el compilador no realiza cálculos de compensación.

    Pero esto está mal, porque D está realmente 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 desde el horrendo -O0ensamblaje que solo 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:

Probado en Ubuntu 18.04 amd64, GCC 7.4.0.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.