todas - ¿Cada función miembro de c++ toma `this` como una entrada implícitamente?
funciones en c++ ejemplos (4)
Cuando creamos una función miembro para una clase en c ++, tiene un argumento adicional implícito que es un puntero al objeto que llama, denominado this
.
¿Es esto cierto para cualquier función, incluso si no usa this
puntero? Por ejemplo, dada la clase.
class foo
{
private:
int bar;
public:
int get_one()
{
return 1; // Not using `this`
}
int get_bar()
{
return this->bar; // Using `this`
}
}
¿Las dos funciones ( get_one
y get_bar
) tomarían this
como un parámetro implícito, aunque solo una de ellas lo use realmente?
Parece un poco inútil hacerlo.
Nota : entiendo que lo correcto sería hacer get_one()
estático, y que la respuesta puede depender de la implementación, pero tengo curiosidad.
¿Las dos funciones (get_one y get_bar) tomarían esto como un parámetro implícito aunque solo lo use onle get_bar?
Sí (a menos que el compilador lo optimice, lo que aún no significa que pueda llamar a la función sin un objeto válido).
Parece un poco inútil hacerlo
Entonces, ¿por qué es un miembro si no utiliza ningún dato del miembro? A veces, el enfoque correcto es convertirlo en una función gratuita en el mismo espacio de nombres.
... clase en c ++, tal como lo entiendo, tiene un argumento adicional implícito que es un puntero al objeto llamante
Es importante tener en cuenta que C ++ comenzó como C con objetos.
Para eso, el puntero de this
no es uno que está implícitamente presente dentro de una función miembro, sino que la función miembro, cuando se compila, necesita una forma de saber a qué se refiere esto; por lo tanto, la noción de un puntero implícito al objeto llamante que se pasa.
Para decirlo de otra manera, tomemos tu clase de C ++ y hagamos una versión C:
C ++
class foo
{
private:
int bar;
public:
int get_one()
{
return 1;
}
int get_bar()
{
return this->bar;
}
int get_foo(int i)
{
return this->bar + i;
}
};
int main(int argc, char** argv)
{
foo f;
printf("%d/n", f.get_one());
printf("%d/n", f.get_bar());
printf("%d/n", f.get_foo(10));
return 0;
}
do
typedef struct foo
{
int bar;
} foo;
int foo_get_one(foo *this)
{
return 1;
}
int foo_get_bar(foo *this)
{
return this->bar;
}
int foo_get_foo(int i, foo *this)
{
return this->bar + i;
}
int main(int argc, char** argv)
{
foo f;
printf("%d/n", foo_get_one(&f));
printf("%d/n", foo_get_bar(&f));
printf("%d/n", foo_get_foo(10, &f));
return 0;
}
Cuando el programa C ++ se compila y ensambla, this
puntero se "agrega" a la función mutilada para "saber" qué objeto está llamando la función miembro.
Entonces foo::get_one
podría ser "destrozado" al equivalente en C de foo_get_one(foo *this)
, foo::get_bar
podría ser foo_get_bar(foo *this)
a foo_get_bar(foo *this)
y foo::get_foo(int)
podría ser foo_get_foo(int, foo *this)
, etc.
¿Las dos funciones (
get_one
yget_bar
) tomarían esto como un parámetro implícito aunque solo unget_bar
use? Parece un poco inútil hacerlo.
Esta es una función del compilador y si no se realizaron optimizaciones en absoluto, las heurísticas aún podrían eliminar this
puntero en una función mutilada donde no es necesario llamar a un objeto (para guardar la pila), pero eso depende en gran medida del código y de cómo Se está compilando y en qué sistema.
Más específicamente, si la función fuera tan simple como foo::get_one
(simplemente devolviendo un 1
), es probable que el compilador simplemente ponga la constante 1
en lugar de la llamada a object->get_one()
, eliminando la necesidad de cualquier Referencias / punteros.
Espero que pueda ayudar.
Si no usas this
, entonces no puedes decir si está disponible. Así que literalmente no hay distinción. Esto es como preguntar si un árbol que cae en un bosque despoblado emite un sonido. Es literalmente una pregunta sin sentido.
Te puedo decir esto: si quieres usar this
en una función miembro, puedes hacerlo. Esa opción siempre está disponible para usted.
Semánticamente, this
puntero siempre está disponible en una función miembro, como lo señaló otro usuario. Es decir, podría luego cambiar la función para usarla sin problema (y, en particular, sin la necesidad de volver a compilar el código de llamada en otras unidades de traducción) o, en el caso de una función virtual
, se podría usar una versión anulada en una subclase. this
incluso si la implementación base no lo hizo.
Entonces, la pregunta interesante que queda es qué impacto impone el rendimiento , si lo hay. Puede haber un costo para la persona que llama y / o la persona que recibe la llamada y el costo puede ser diferente cuando está en línea y no en línea. Examinamos todas las permutaciones a continuación:
En línea
En el caso en línea , el compilador puede ver tanto el sitio de la llamada como la implementación de la función 1 , por lo que presumiblemente no necesita seguir ninguna convención de llamada en particular y, por lo tanto, el costo del puntero oculto debería desaparecer. Tenga en cuenta también que, en este caso, no hay una distinción real entre el código del "destinatario" y el código "llamado", ya que se combinan en forma óptima en el sitio de la llamada.
Usemos el siguiente código de prueba:
#include <stdio.h>
class foo
{
private:
int bar;
public:
int get_one_member()
{
return 1; // Not using `this`
}
};
int get_one_global() {
return 2;
}
int main(int argc, char **) {
foo f = foo();
if(argc) {
puts("a");
return f.get_one_member();
} else {
puts("b");
return get_one_global();
}
}
Tenga en cuenta que las dos llamadas de llamada están ahí para hacer que las ramas sean un poco más diferentes; de lo contrario, los compiladores son lo suficientemente inteligentes como para usar solo un conjunto / movimiento condicional, por lo que ni siquiera pueden separar los cuerpos en línea de las dos funciones.
Todo gcc , icc y clang integran las dos llamadas y generan un código que es equivalente tanto para la función miembro como para la función no miembro, sin ningún rastro de this
puntero en el caso del miembro. Veamos el código de clang
, ya que es el más limpio:
main:
push rax
test edi,edi
je 400556 <main+0x16>
# this is the member case
mov edi,0x4005f4
call 400400 <puts@plt>
mov eax,0x1
pop rcx
ret
# this is the non-member case
mov edi,0x4005f6
call 400400 <puts@plt>
mov eax,0x2
pop rcx
ret
Ambas rutas generan exactamente la misma serie de 4 instrucciones previas a la última ret
: dos instrucciones para la llamada a las llamadas, una sola instrucción para mov
el valor de retorno de 1
o 2
a eax
, y un pop rcx
para limpiar la pila 2 . Por lo tanto, la llamada real tomó exactamente una instrucción en cualquier caso, y no hubo ninguna manipulación de puntero o pase en absoluto.
Fuera de línea
En los costos fuera de línea, el soporte de this
indicador realmente tendrá algunos costos reales pero generalmente pequeños, al menos en el lado de la persona que llama.
Usamos un programa de prueba similar, pero con las funciones miembro declaradas fuera de línea y con la inclusión de esas funciones desactivadas 3 :
class foo
{
private:
int bar;
public:
int __attribute__ ((noinline)) get_one_member();
};
int foo::get_one_member()
{
return 1; // Not using `this`
}
int __attribute__ ((noinline)) get_one_global() {
return 2;
}
int main(int argc, char **) {
foo f = foo();
return argc ? f.get_one_member() :get_one_global();
}
Este código de prueba es algo más simple que el anterior porque no necesita la llamada a las llamadas para distinguir las dos ramas.
Llamar al sitio
Veamos el conjunto que generates gcc
4 para main
(es decir, en los sitios de llamadas para las funciones):
main:
test edi,edi
jne 400409 <main+0x9>
# the global branch
jmp 400530 <get_one_global()>
# the member branch
lea rdi,[rsp-0x18]
jmp 400520 <foo::get_one_member()>
nop WORD PTR cs:[rax+rax*1+0x0]
nop DWORD PTR [rax]
En este caso, ambas llamadas de función se realizan realmente mediante jmp
: este es un tipo de optimización de llamada de cola, ya que son las últimas funciones llamadas en main, por lo que el ret
para la función llamada realmente regresa al llamador de main
, pero aquí el llamador de La función miembro paga un precio extra:
lea rdi,[rsp-0x18]
Eso es cargar el puntero de this
en la pila en rdi
que recibe el primer argumento que es this
para las funciones de miembro de C ++. Así que hay un costo extra (pequeño).
Cuerpo de funciones
Ahora, mientras que el sitio de llamada paga un cierto costo para pasar un puntero (sin usar), en este caso al menos, los cuerpos de funciones reales son igualmente eficientes:
foo::get_one_member():
mov eax,0x1
ret
get_one_global():
mov eax,0x2
ret
Ambos están compuestos de un solo mov
y un ret
. Así que la función en sí misma puede simplemente ignorar this
valor, ya que no se utiliza.
Esto plantea la cuestión de si esto es cierto en general: ¿el cuerpo de la función de una función miembro que no usa this
siempre se compilará tan eficientemente como una función no miembro equivalente?
La respuesta corta es no , al menos para la mayoría de las ABI modernas que pasan argumentos en los registros. El puntero de this
toma ocupa un registro de parámetros en la convención de llamada, por lo que alcanzará el número máximo de argumentos de registro pasado un parámetro antes al compilar una función miembro.
Tomemos, por ejemplo, esta función que simplemente suma sus seis parámetros int
juntos:
int add6(int a, int b, int c, int d, int e, int f) {
return a + b + c + d + e + f;
}
Cuando se compila como una función miembro en una plataforma x86-64 utilizando el SysV ABI , tendrá que pasar el registro en la pila para la función miembro, lo que resultará en un código como este :
foo::add6_member(int, int, int, int, int, int):
add esi,edx
mov eax,DWORD PTR [rsp+0x8]
add ecx,esi
add ecx,r8d
add ecx,r9d
add eax,ecx
ret
Tenga en cuenta la lectura de la pila eax,DWORD PTR [rsp+0x8]
que generalmente agregará algunos ciclos de latencia 5 y una instrucción en gcc 6 en comparación con la versión no miembro, que no tiene lectura de memoria:
add6_nonmember(int, int, int, int, int, int):
add edi,esi
add edx,edi
add ecx,edx
add ecx,r8d
lea eax,[rcx+r9*1]
ret
Por lo general, no tendrá seis o más argumentos para una función (especialmente los muy cortos, sensibles al rendimiento), pero al menos esto demuestra que incluso en el lado de la generación de código del interlocutor, this
puntero oculto no siempre es gratuito.
Tenga en cuenta también que si bien los ejemplos utilizaron el código x86-64 y el SysV ABI, los mismos principios básicos se aplicarían a cualquier ABI que pase algunos argumentos en los registros.
1 Tenga en cuenta que esta optimización solo se aplica fácilmente a funciones efectivamente no virtuales, ya que solo entonces el compilador puede conocer la implementación de la función real.
2 Supongo que para eso está: deshace el push rax
en la parte superior del método para que rsp
tenga el valor correcto en el retorno, pero no sé por qué el par push/pop
debe estar allí en primer lugar . Otros compiladores utilizan diferentes estrategias, como add rsp, 8
y sub rsp,8
.
3 En la práctica, realmente no se va a deshabilitar la inline así, pero la falla en la línea podría ocurrir solo porque los métodos están en diferentes unidades de compilación. Debido a la forma en que funciona Godbolt, no puedo hacer eso exactamente, por lo que deshabilitar el ingreso en línea tiene el mismo efecto.
4 Por extraño que parezca, no pude hacer que el clang
deje de incluir ninguna de las funciones, ya sea con el atributo noinline
o con -fno-inline
.
5 De hecho, a menudo algunos ciclos superan la latencia habitual de L1 de 4 ciclos en Intel, debido al reenvío de la tienda del valor escrito recientemente.
6 En principio, al menos en x86, la penalización de una sola instrucción puede eliminarse mediante el uso de un add
con un operando de origen de memoria, en lugar de un mov
de la memoria con un add
registro posterior y, de hecho, clang y clang hacen exactamente eso. Sin embargo, no creo que un enfoque domine: el enfoque de gcc
con un movimiento separado es más capaz de mover la carga de la ruta crítica: iniciarla temprano y luego usarla solo en la última instrucción, mientras que el enfoque icc
agrega 1 ciclo a La ruta crítica que involucra el enfoque de mov
y clang
parece la peor de todas: unir todos los agregados en una larga cadena de dependencia en eax
que termina con la lectura de la memoria.