c++ - ¿Es el puntero de "esto" solo un compilatorio?
gcc this (12)
Entonces, ¿el puntero de este solo es un compilador y no un puntero real?
Es mucho una cuestión de tiempo de ejecución. Se refiere al objeto en el que se invoca la función miembro, naturalmente ese objeto puede existir en tiempo de ejecución.
Lo que es una cuestión de tiempo de compilación es cómo funciona la búsqueda de nombres. Cuando un compilador se encuentra con x = X
, debe averiguar qué es este x
que se está asignando. Así que lo busca, y encuentra la variable miembro. Dado que this->x
y x
refieren a lo mismo, naturalmente obtendrá la misma salida de ensamblaje.
Me pregunté si this
puntero podría ser usado en exceso, ya que generalmente lo uso cada vez que me refiero a una función o variable miembro. Me pregunté si podría tener un impacto en el rendimiento, ya que debe haber un puntero que debe ser referenciado cada vez. Así que escribí un código de prueba
struct A {
int x;
A(int X) {
x = X; /* And a second time with this->x = X; */
}
};
int main() {
A a(8);
return 0;
}
y sorprendentemente, incluso con -O0
emiten exactamente el mismo código de ensamblador.
Además, si uso una función miembro y la llamo en otra función miembro, muestra el mismo comportamiento. Entonces, ¿el puntero de this
solo es un compilador y no un puntero real? ¿O hay casos en que this
es traducido y desreferenciado? Yo uso GCC 4.4.3 por cierto.
ya que generalmente lo uso cada vez que me refiero a una función o variable miembro.
Siempre usas this
cuando te refieres a una variable o función miembro. Simplemente no hay otra manera de llegar a los miembros. La única opción es la notación implícita vs explícita.
Regresemos para ver cómo se hizo antes de this
para entender qué es this
.
Sin OOP:
struct A {
int x;
};
void foo(A* that) {
bar(that->x)
}
Con OOP pero escribiendo this
explícitamente
struct A {
int x;
void foo(void) {
bar(this->x)
}
};
usando notación más corta:
struct A {
int x;
void foo(void) {
bar(x)
}
};
Pero la diferencia está solo en el código fuente. Todos están compilados para lo mismo. Si creas un método miembro, el compilador creará un argumento de puntero para ti y lo llamará "esto". Si omite this->
cuando se refiere a un miembro, el compilador es lo suficientemente inteligente como para insertarlo la mayor parte del tiempo. Eso es. La única diferencia es 6 letras menos en la fuente.
Escribir this
explícitamente tiene sentido cuando hay una ambigüedad, es decir, otra variable llamada como su variable miembro:
struct A {
int x;
A(int x) {
this->x = x
}
};
Hay algunos casos, como __thiscall, en los que el código OO y el código que no es OO pueden terminar con un bit diferente en asm, pero siempre que el puntero se pasa a la pila y luego se optimiza a un registro o en ECX desde el principio no lo hace "no un puntero ".
"esto" también puede proteger contra el sombreado mediante un parámetro de función, por ejemplo:
class Vector {
public:
double x,y,z;
void SetLocation(double x, double y, double z);
};
void Vector::SetLocation(double x, double y, double z) {
this->x = x; //Passed parameter assigned to member variable
this->y = y;
this->z = z;
}
(Obviamente, no se recomienda escribir dicho código).
Aquí hay un ejemplo simple de cómo "esto" podría ser útil durante el tiempo de ejecución:
#include <vector>
#include <string>
#include <iostream>
class A;
typedef std::vector<A*> News;
class A
{
public:
A(const char* n): name(n){}
std::string name;
void subscribe(News& n)
{
n.push_back(this);
}
};
int main()
{
A a1("Alex"), a2("Bob"), a3("Chris");
News news;
a1.subscribe(news);
a3.subscribe(news);
std::cout << "Subscriber:";
for(auto& a: news)
{
std::cout << " " << a->name;
}
return 0;
}
Después de la compilación, cada símbolo es solo una dirección, por lo que no puede ser un problema de tiempo de ejecución.
Cualquier símbolo de miembro se compila para un desplazamiento en la clase actual de todos modos, incluso si no usó this
.
Cuando se usa el name
en C ++, puede ser uno de los siguientes.
- En el espacio de nombres global (como
::name
), o en el espacio de nombres actual, o en el espacio de nombres usado (cuando seusing namespace ...
ha utilizado) - En la clase actual
- Definición local, en bloque superior.
- Definición local, en bloque actual.
Por lo tanto, cuando escribe código, el compilador debe escanear cada uno, de manera que busque el nombre del símbolo, desde el bloque actual hasta el espacio de nombres global.
Usar this->name
ayuda al compilador a restringir la búsqueda de name
para buscarlo solo en el ámbito de la clase actual, lo que significa que omite las definiciones locales y, si no se encuentra en el ámbito de la clase, no lo busque en el ámbito global.
Es un puntero real, como lo especifica la norma (§12.2.2.1):
En el cuerpo de una función miembro no estática (12.2.1), la palabra clave es una expresión prvalue cuyo valor es la dirección del objeto para el que se llama la función. El tipo de
this
en una función miembro de una claseX
esX*
.
this
es realmente implícito cada vez que hace referencia a una variable miembro no estática o una función miembro dentro de un código propio de clase. También es necesario (ya sea implícito o explícito) porque el compilador necesita vincular la función o la variable a un objeto real en tiempo de ejecución.
Su uso explícito rara vez es útil, a menos que necesite, por ejemplo, desambiguar entre un parámetro y una variable miembro dentro de una función miembro. De lo contrario, sin él, el compilador ocultará la variable miembro con el parámetro ( Véalo en vivo en Coliru ).
Esto es casi un duplicado de ¿Cómo funcionan los objetos en x86 en el nivel de ensamblaje? , donde comento la salida de asm de algunos ejemplos, incluida la visualización de qué registro se pasó this
puntero.
En asm, this
funciona exactamente igual que un primer argumento oculto , por lo tanto tanto la función miembro foo::add(int)
como el add
no miembro que lleva una compilación explícita foo*
primer argumento a exactamente el mismo asm.
struct foo {
int m;
void add(int a); // not inline so we get a stand-alone definition emitted
};
void foo::add(int a) {
this->m += a;
}
void add(foo *obj, int a) {
obj->m += a;
}
En el explorador del compilador Godbolt , compilando para x86-64 con el System V ABI (primer argumento en RDI, segundo en RSI), obtenemos:
# gcc8.2 -O3
foo::add(int):
add DWORD PTR [rdi], esi # memory-destination add
ret
add(foo*, int):
add DWORD PTR [rdi], esi
ret
Yo uso GCC 4.4.3
Se lanzó en enero de 2010 , por lo que faltan casi una década de mejoras en el optimizador y en los mensajes de error. La serie gcc7 ha estado fuera y estable por un tiempo. Espere optimizaciones perdidas con un compilador tan viejo, especialmente para conjuntos de instrucciones modernos como AVX.
Si el compilador alinea una función miembro que se llama con enlace estático en lugar de dinámico, podría ser capaz de optimizar el puntero. Tomemos este sencillo ejemplo:
#include <iostream>
using std::cout;
using std::endl;
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int main(void)
{
example e;
e.foo(10);
cout << e.foo() << endl;
}
GCC 7.3.0 con el indicador -march=x86-64 -O -S
puede compilar cout << e.foo()
en tres instrucciones:
movl $10, %esi
leaq _ZSt4cout(%rip), %rdi
call _ZNSolsEi@PLT
Esta es una llamada a std::ostream::operator<<
. Recuerda que cout << e.foo();
es azúcar sintáctico para std::ostream::operator<< (cout, e.foo());
. Y el operator<<(int)
podría escribirse de dos maneras: static operator<< (ostream&, int)
, como una función no miembro, donde el operando de la izquierda es un parámetro explícito, u operator<<(int)
, como Una función miembro, donde está implícitamente this
.
El compilador pudo deducir que e.foo()
siempre será la constante 10
. Dado que la convención de llamada x86 de 64 bits es pasar los argumentos de la función en los registros, que se compilan en la única instrucción movl
, que establece el segundo parámetro de la función en 10
. La instrucción leaq
establece el primer argumento (que podría ser un ostream&
explícito ostream&
el implícito this
) en &cout
. Entonces el programa hace una call
a la función.
Sin embargo, en casos más complejos, como por ejemplo si tiene una función tomando un example&
como parámetro, el compilador debe buscar this
, ya que this
es lo que le dice al programa con qué instancia está trabajando, y por lo tanto, con qué miembro de datos x
la instancia para buscar.
Considera este ejemplo:
class example {
public:
int foo() const { return x; }
int foo(const int i) { return (x = i); }
private:
int x;
};
int bar( const example& e )
{
return e.foo();
}
La bar()
funciones bar()
se compila en un poco de repetitivo y la instrucción:
movl (%rdi), %eax
ret
Recuerda del ejemplo anterior que %rdi
en x86-64 es el primer argumento de la función, el puntero implícito para la llamada a e.foo()
. Ponerlo entre paréntesis, (%rdi)
, significa buscar la variable en esa ubicación. (Dado que los únicos datos en una instancia de example
son x
, &e.x
resulta ser el mismo que &e
en este caso). Mover el contenido a %eax
establece el valor de retorno.
En este caso, el compilador necesitaba el argumento implícito para foo(/* example* this */)
para poder encontrar &e
y, por tanto, &e.x
De hecho, dentro de una función miembro (que no es static
), x
, this->x
y (*this).x
significan lo mismo.
Su máquina no sabe nada acerca de los métodos de clase, son funciones normales debajo del capó. Por lo tanto, los métodos deben implementarse pasando siempre un puntero al objeto actual, simplemente está implícito en C ++, es decir, la T Class::method(...)
es solo azúcar sintáctica para el T Class_Method(Class* this, ...)
.
Otros lenguajes como Python o Lua eligen hacerlo explícito y las API de C orientadas a objetos modernos como Vulkan (a diferencia de OpenGL) usan un patrón similar.
this
es de hecho un indicador de tiempo de ejecución (aunque uno suministrado implícitamente por el compilador), como se ha repetido en la mayoría de las respuestas. Se utiliza para indicar en qué instancia de una clase operará una función miembro cuando se llama; para cualquier instancia dada c
de la clase C
, cuando se llama a cualquier función miembro cf()
, c.cf()
recibirá un puntero igual a &c
(esto naturalmente también se aplica a cualquier estructura de tipo S
, cuando se llama a la función miembro s.sf()
, como se utilizará para demostraciones más limpias). Incluso puede ser cv calificado como cualquier otro puntero, con los mismos efectos (pero, desafortunadamente, no es la misma sintaxis debido a que es especial); Esto se usa comúnmente para la corrección de const
, y mucho menos frecuentemente para la corrección volatile
.
template<typename T>
uintptr_t addr_out(T* ptr) { return reinterpret_cast<uintptr_t>(ptr); }
struct S {
int i;
uintptr_t address() const { return addr_out(this); }
};
// Format a given numerical value into a hex value for easy display.
// Implementation omitted for brevity.
template<typename T>
std::string hex_out_s(T val, bool disp0X = true);
// ...
S s[2];
std::cout << "Control example: Two distinct instances of simple class./n";
std::cout << "s[0] address:/t/t/t/t" << hex_out_s(addr_out(&s[0]))
<< "/n* s[0] this pointer:/t/t/t" << hex_out_s(s[0].address())
<< "/n/n";
std::cout << "s[1] address:/t/t/t/t" << hex_out_s(addr_out(&s[1]))
<< "/n* s[1] this pointer:/t/t/t" << hex_out_s(s[1].address())
<< "/n/n";
Salida de muestra:
Control example: Two distinct instances of simple class.
s[0] address: 0x0000003836e8fb40
* s[0] this pointer: 0x0000003836e8fb40
s[1] address: 0x0000003836e8fb44
* s[1] this pointer: 0x0000003836e8fb44
Estos valores no están garantizados y pueden cambiar fácilmente de una ejecución a la siguiente; Esto se puede observar más fácilmente al crear y probar un programa, mediante el uso de herramientas de compilación.
Mecánicamente, es similar a un parámetro oculto agregado al inicio de la lista de argumentos de cada función miembro; xf() cv
puede verse como una variante especial de f(cv X* this)
, aunque con un formato diferente por razones lingüísticas. De hecho, hubo propuestas recientes tanto de Stroustrup como de Sutter para unificar la sintaxis de llamada de xf(y)
f(x, y)
, lo que hubiera hecho de este comportamiento implícito una regla lingüística explícita. Desafortunadamente, se encontró con la preocupación de que podría causar algunas sorpresas no deseadas para los desarrolladores de bibliotecas, y por lo tanto aún no se ha implementado; que yo sepa, la propuesta más reciente es una propuesta conjunta, para que f(x,y)
pueda recurrir a xf(y)
si no se encuentra f(x,y)
, similar a la interacción entre, por ejemplo, std::begin(x)
y la función miembro x.begin()
.
En este caso, this
sería más parecido a un puntero normal, y el programador podría especificarlo manualmente. Si se encuentra que una solución permite la forma más robusta sin violar el principio de menos asombro (o hacer pasar cualquier otra inquietud), entonces un equivalente a this
también podría generarse implícitamente como un puntero normal para funciones que no son miembros , también.
De manera relacionada, una cosa importante a tener en cuenta es que this
es la dirección de la instancia, como se ve en esa instancia ; Si bien el puntero en sí es una cosa del tiempo de ejecución, no siempre tiene el valor que uno pensaría que tiene. Esto se vuelve relevante cuando se observan clases con jerarquías de herencia más complejas. Específicamente, cuando se observan casos en los que una o más clases base que contienen funciones miembro no tienen la misma dirección que la propia clase derivada. Tres casos en particular vienen a la mente:
Tenga en cuenta que estos se demuestran utilizando MSVC, con salida de diseños de clase a través del parámetro del compilador no documentado -d1reportSingleClassLayout , debido a que lo encuentro más fácil de leer que los equivalentes de GCC o Clang.
Diseño no estándar: cuando una clase es diseño estándar, la dirección del primer miembro de datos de una instancia es exactamente idéntica a la dirección de la instancia en sí misma; por lo tanto, se puede decir que esto es equivalente a la dirección del primer miembro de datos. Esto será cierto incluso si dicho miembro de datos es miembro de una clase base, siempre y cuando la clase derivada continúe siguiendo las reglas de diseño estándar. ... Por el contrario, esto también significa que si la clase derivada no es un diseño estándar, esto ya no está garantizado.
struct StandardBase { int i; uintptr_t address() const { return addr_out(this); } }; struct NonStandardDerived : StandardBase { virtual void f() {} uintptr_t address() const { return addr_out(this); } }; static_assert(std::is_standard_layout<StandardBase>::value, "Nyeh."); static_assert(!std::is_standard_layout<NonStandardDerived>::value, ".heyN"); // ... NonStandardDerived n; std::cout << "Derived class with non-standard layout:" << "/n* n address:/t/t/t/t/t" << hex_out_s(addr_out(&n)) << "/n* n this pointer:/t/t/t/t" << hex_out_s(n.address()) << "/n* n this pointer (as StandardBase):/t/t" << hex_out_s(n.StandardBase::address()) << "/n* n this pointer (as NonStandardDerived):/t" << hex_out_s(n.NonStandardDerived::address()) << "/n/n";
Salida de muestra:
Derived class with non-standard layout: * n address: 0x00000061e86cf3c0 * n this pointer: 0x00000061e86cf3c0 * n this pointer (as StandardBase): 0x00000061e86cf3c8 * n this pointer (as NonStandardDerived): 0x00000061e86cf3c0
Tenga en cuenta que
StandardBase::address()
se suministra con un puntero diferente al deNonStandardDerived::address()
, incluso cuando se llama en la misma instancia. Esto se debe a que el uso de este último de un vtable hizo que el compilador inserte un miembro oculto.class StandardBase size(4): +--- 0 | i +--- class NonStandardDerived size(16): +--- 0 | {vfptr} | +--- (base class StandardBase) 8 | | i | +--- | <alignment member> (size=4) +--- NonStandardDerived::$vftable@: | &NonStandardDerived_meta | 0 0 | &NonStandardDerived::f NonStandardDerived::f this adjustor: 0
Clases de base virtual: debido a que las bases virtuales se ubican después de la clase más derivada,
this
puntero suministrado a una función miembro heredada de una base virtual será diferente del que se proporcionó a los miembros de la clase derivada.struct VBase { uintptr_t address() const { return addr_out(this); } }; struct VDerived : virtual VBase { uintptr_t address() const { return addr_out(this); } }; // ... VDerived v; std::cout << "Derived class with virtual base:" << "/n* v address:/t/t/t/t/t" << hex_out_s(addr_out(&v)) << "/n* v this pointer:/t/t/t/t" << hex_out_s(v.address()) << "/n* this pointer (as VBase):/t/t/t" << hex_out_s(v.VBase::address()) << "/n* this pointer (as VDerived):/t/t/t" << hex_out_s(v.VDerived::address()) << "/n/n";
Salida de muestra:
Derived class with virtual base: * v address: 0x0000008f8314f8b0 * v this pointer: 0x0000008f8314f8b0 * this pointer (as VBase): 0x0000008f8314f8b8 * this pointer (as VDerived): 0x0000008f8314f8b0
Una vez más, la función miembro de la clase base se suministra con
this
puntero diferente, debido a que elVDerived
heredado deVBase
tiene una dirección de inicio diferente a la deVDerived
.class VDerived size(8): +--- 0 | {vbptr} +--- +--- (virtual base VBase) +--- VDerived::$vbtable@: 0 | 0 1 | 8 (VDerivedd(VDerived+0)VBase) vbi: class offset o.vbptr o.vbte fVtorDisp VBase 8 0 4 0
Herencia múltiple: como se puede esperar, la herencia múltiple puede llevar fácilmente a casos en los que el puntero pasado a una función miembro sea diferente al que
this
puntero pasó a una función miembro diferente, incluso si ambas funciones se llaman con la misma instancia. Esto puede surgir para funciones miembro de cualquier clase base que no sea la primera, de manera similar a cuando se trabaja con clases de diseño no estándar (donde todas las clases base después del primer inicio tienen una dirección diferente a la clase derivada) ... pero Puede ser especialmente sorprendente en el caso devirtual
funcionesvirtual
, cuando varios miembros suministran funciones virtuales con la misma firma.struct Base1 { int i; virtual uintptr_t address() const { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; struct Base2 { short s; virtual uintptr_t address() const { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; struct Derived : Base1, Base2 { bool b; uintptr_t address() const override { return addr_out(this); } uintptr_t raw_address() { return addr_out(this); } }; // ... Derived d; std::cout << "Derived class with multiple inheritance:" << "/n (Calling address() through a static_cast reference, then the appropriate raw_address().)" << "/n* d address:/t/t/t/t/t" << hex_out_s(addr_out(&d)) << "/n* d this pointer:/t/t/t/t" << hex_out_s(d.address()) << " (" << hex_out_s(d.raw_address()) << ")" << "/n* d this pointer (as Base1):/t/t/t" << hex_out_s(static_cast<Base1&>((d)).address()) << " (" << hex_out_s(d.Base1::raw_address()) << ")" << "/n* d this pointer (as Base2):/t/t/t" << hex_out_s(static_cast<Base2&>((d)).address()) << " (" << hex_out_s(d.Base2::raw_address()) << ")" << "/n* d this pointer (as Derived):/t/t/t" << hex_out_s(static_cast<Derived&>((d)).address()) << " (" << hex_out_s(d.Derived::raw_address()) << ")" << "/n/n";
Salida de muestra:
Derived class with multiple inheritance: (Calling address() through a static_cast reference, then the appropriate raw_address().) * d address: 0x00000056911ef530 * d this pointer: 0x00000056911ef530 (0x00000056911ef530) * d this pointer (as Base1): 0x00000056911ef530 (0x00000056911ef530) * d this pointer (as Base2): 0x00000056911ef530 (0x00000056911ef540) * d this pointer (as Derived): 0x00000056911ef530 (0x00000056911ef530)
Esperamos que cada
raw_address()
mismas reglas debido a que cada una es explícitamente una función separada, y por lo tanto,Base2::raw_address()
devolverá un valor diferente al deDerived::raw_address()
. Pero como sabemos que las funciones derivadas siempre llamarán la forma más derivada, ¿cómo es correcta laaddress()
cuando se llama desde una referencia aBase2
? Esto se debe a un pequeño truco del compilador llamado "thor de ajuste", que es un ayudante que tomathis
puntero de una instancia de clase base y lo ajusta para que apunte a la clase más derivada cuando sea necesario.class Derived size(40): +--- | +--- (base class Base1) 0 | | {vfptr} 8 | | i | | <alignment member> (size=4) | +--- | +--- (base class Base2) 16 | | {vfptr} 24 | | s | | <alignment member> (size=6) | +--- 32 | b | <alignment member> (size=7) +--- Derived::$vftable@Base1@: | &Derived_meta | 0 0 | &Derived::address Derived::$vftable@Base2@: | -16 0 | &thunk: this-=16; goto Derived::address Derived::address this adjustor: 0
Si tienes curiosidad, siéntete libre de jugar un poco con este pequeño programa , para ver cómo cambian las direcciones si lo ejecutas varias veces, o en los casos en que tenga un valor diferente al que puedes esperar.
this
es un puntero Es como un parámetro implícito que forma parte de cada método. Podrías imaginarte usando funciones C simples y escribiendo código como:
Socket makeSocket(int port) { ... }
void send(Socket *this, Value v) { ... }
Value receive(Socket *this) { ... }
Socket *mySocket = makeSocket(1234);
send(mySocket, someValue); // The subject, `mySocket`, is passed in as a param called "this", explicitly
Value newData = receive(socket);
En C ++, un código similar podría verse como:
mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();
this
siempre tiene que existir cuando estás en un método no estático. Ya sea que lo use explícitamente o no, debe tener una referencia a la instancia actual, y esto es lo que this
le brinda.
En ambos casos, accederá a la memoria a través de this
puntero. Es solo que puedes omitirlo en algunos casos.