2012-09-18 58 views
9

tengo este código:¿Se permite que un objeto cambie legalmente su tipo durante su vigencia en C++?

class Class { 
public: 
    virtual void first() {}; 
    virtual void second() {}; 
}; 

Class* object = new Class(); 
object->first(); 
object->second(); 
delete object; 

que compilar con Visual C++ 10 con/O2 y tener esta desmontaje:

282: Class* object = new Class(); 
00403953 push  4 
00403955 call  dword ptr [__imp_operator new (4050BCh)] 
0040395B add   esp,4 
0040395E test  eax,eax 
00403960 je   wmain+1Ch (40396Ch) 
00403962 mov   dword ptr [eax],offset Class::`vftable' (4056A4h) 
00403968 mov   esi,eax 
0040396A jmp   wmain+1Eh (40396Eh) 
0040396C xor   esi,esi 
283: object->first(); 
0040396E mov   eax,dword ptr [esi] 
00403970 mov   edx,dword ptr [eax] 
00403972 mov   ecx,esi 
00403974 call  edx 
284: object->second(); 
00403976 mov   eax,dword ptr [esi] 
00403978 mov   edx,dword ptr [eax+4] 
0040397B mov   ecx,esi 
0040397D call  edx 
285: delete object; 
0040397F push  esi 
00403980 call  dword ptr [__imp_operator delete (405138h)] 

Tenga en cuenta que en 00403968 la dirección del inicio de objetos (donde vptr es almacenado) se copia en el registro esi. Luego, al 0040396E esta dirección se usa para recuperar el vptr y el valor vptr se usa para recuperar la dirección de first(). Luego, al 00403976, se recupera el vptr y se usa para recuperar la dirección de second().

¿Por qué se recupera vptr dos veces? ¿Podría el objeto posible tener su vptr cambiado entre llamadas o es simplemente una anulación de la privación?

+0

Weird. Pensé que todo debería ser un no-op. Es probable que sea una excepción, si se mira dentro de las funciones podría haber visto que ninguno de ellos cambia 'esi'. –

Respuesta

9

¿Por qué se recupera vptr dos veces? ¿Podría el objeto posible tener su vptr cambiado entre llamadas o es simplemente una anulación de la privación?

considerar:

object->first(); 

Esta llamada puede destruir el objeto y crear una nueva en el mismo trozo de memoria. Por lo tanto, después de esta llamada no se pueden hacer suposiciones sobre el estado. Ej .:

#include <new> 

struct Class { 
    virtual void first(); 
    virtual void second() {} 
    virtual ~Class() {} 
}; 

struct OtherClass : Class { 
    void first() {} 
    void second() {} 
}; 

void Class::first() { 
    void* p = this; 
    static_assert(sizeof(Class) == sizeof(OtherClass), "Oops"); 
    this->~Class(); 
    new (p) OtherClass; 
} 

int main() { 
    Class* object = new Class(); 
    object->first(); 
    object->second(); 
    delete object; 
} 

compiladores pueden optimizar las cargas de distancia de registro innecesarias si esa función es en línea y/o se utiliza la generación de código en tiempo de enlace.


Como DeadMG y Steve Jessop observaron que el código anterior muestra un comportamiento indefinido. De acuerdo con 3.8/7 de C++ 2003 Standard:

Si, después de que el tiempo de vida de un objeto ha terminado y antes de que el almacenamiento de la que el objeto ocupada se reutiliza o se libera, un nuevo objeto se crea en la ubicación de almacenamiento que la objeto original ocupado, un puntero que apunta al objeto original, una referencia que hace referencia al objeto original, o el nombre del objeto original se referirá automáticamente al nuevo objeto y, una vez que se haya iniciado el tiempo de vida del nuevo objeto, puede ser utilizado para manipular el nuevo objeto, si:

  • el almacenamiento del nuevo objeto se superpone exactamente a la ubicación de almacenamiento que ocupó el objeto original, y
  • el nuevo objeto es del mismo tipo que el objeto original (ignorando los de nivel superior CV-calificadores) y
  • del tipo de objeto original no es const cualificado, y, si es un tipo de clase, hace no contiene ningún miembro de datos no estáticos cuyo tipo es const-calificado o un tipo de referencia, y
  • el objeto original era un objeto más derivado (1.8) de tipo T y el nuevo objeto es un objeto más derivado de tipo T (es decir, no son subobjetos de clase base).

El código anterior no satisface el requisito 2 de la lista anterior.

+0

La generación de código de tiempo de enlace no ayuda, pasa lo mismo. –

+1

@LuchianGrigore: Tal vez LTCG no ayude * aún *, pero es una optimización que está disponible para futuros compiladores potenciales. –

+0

@LuchianGrigore: gcc-4.7.1 optimiza el código original como se publicó en una llamada a 'operator new()' seguido de una llamada a 'operator delete()' con '-O3'. ¿Le importa proporcionar alguna evidencia de respaldo para su reclamo? –

2

Se almacena en esi para guardarse entre las llamadas a diferentes funciones.

La convención Microsoft dice

El compilador genera prólogo y epílogo de código para guardar y restaurar las ESI, EDI, EBX, y registros de EBP, si se utilizan en la función.

manera que el puntero almacenado en esi permanecerán, pero el puntero this en ecx no.

+1

Bien, el inicio del objeto se almacena en 'esi', pero ¿por qué' vptr' lee dos veces? – sharptooth

+0

También mantener el 'vptr' requeriría un registro adicional, como' edi', que luego tendría que guardarse y restaurarse. ¿Eso mejoraría el código? Difícil de decir. –

2

Para responder a la pregunta formulada por primera vez el título:

Sí, un objeto de una clase derivada cambia su tipo durante la construcción y la destrucción. Este es el único caso.

El código en el cuerpo de la pregunta es diferente. Pero como Maxim señala correctamente, solo tienes un puntero. Este puntero puede señalar (en diferentes momentos) a dos objetos diferentes que residen en la misma dirección.

Cuestiones relacionadas