Je me suis demandé si le pointeur this
pouvait être trop utilisé, car je l'utilise habituellement à chaque fois que je fais référence à une variable membre ou à une fonction. Je me demandais si cela pouvait avoir un impact sur les performances car il devait y avoir un pointeur qui devait être déréférencé à chaque fois. J'ai donc écrit du code de test
struct A {
int x;
A(int X) {
x = X; /* And a second time with this->x = X; */
}
};
int main() {
A a(8);
return 0;
}
et étonnamment, même avec -O0
, ils produisent exactement le même code assembleur.
De plus, si j'utilise une fonction membre et que je l'appelle dans une autre fonction membre, le comportement est identique. Ainsi, le pointeur this
est-il simplement un élément de compilation et non un pointeur réel? Ou existe-t-il des cas où this
est réellement traduit et déréférencé? J'utilise GCC 4.4.3 btw.
Ainsi, le pointeur this est-il simplement une opération de compilation et non un pointeur réel?
C'est très est une chose d'exécution. Il fait référence à l'objet sur lequel la fonction membre est appelée. Cet objet peut naturellement exister au moment de l'exécution.
Qu'est-ce que est une chose à la compilation est comment fonctionne la recherche de nom. Lorsqu'un compilateur rencontre x = X
, il doit déterminer quelle est cette x
qui est affectée. Donc, il le recherche et trouve la variable membre. Puisque this->x
et x
se réfèrent à la même chose, vous obtenez naturellement la même sortie Assembly.
C'est un pointeur réel, comme le spécifie la norme (§12.2.2.1):
Dans le corps d'une fonction membre non statique (12.2.1), le mot clé
this
est une expression prvalue dont la valeur est l'adresse de l'objet pour lequel la fonction est appelée. Le type dethis
dans une fonction membre d'une classeX
estX*
.
this
est en fait implicite chaque fois que vous faites référence à une variable membre non statique ou à une fonction membre au sein d'un code appartenant à une classe. Cela est également nécessaire (que ce soit implicite ou explicite) car le compilateur doit associer la fonction ou la variable à un objet réel au moment de l'exécution.
Son utilisation explicite est rarement utile, sauf si vous avez besoin, par exemple, de faire la distinction entre un paramètre et une variable membre dans une fonction membre. Sinon, le compilateur masquera la variable membre avec le paramètre ( Voir en direct sur Colir ).
this
doit toujours exister lorsque vous utilisez une méthode non statique. Que vous l'utilisiez explicitement ou non, vous devez avoir une référence à l'instance actuelle, et c'est ce que vous donne this
.
Dans les deux cas, vous allez accéder à la mémoire via le pointeur this
. C'est juste que vous pouvez l'omettre dans certains cas.
Ceci est presque une copie de Comment les objets fonctionnent-ils dans x86 au niveau de Assembly? , où je commente la sortie asm de certains exemples, notamment en indiquant le registre dans lequel le pointeur this
a été passé.
Dans asm, this
fonctionne exactement comme un premier argument masqué , de sorte que la fonction membre foo::add(int)
et le non-membre add
qui prend un explicite foo*
premier argument à compiler exactement le même 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;
}
Sur l'explorateur du compilateur Godbolt , en compilant pour x86-64 avec ABI System V (premier argument dans RDI, deuxième dans RSI), nous obtenons:
# gcc8.2 -O3
foo::add(int):
add DWORD PTR [rdi], esi # memory-destination add
ret
add(foo*, int):
add DWORD PTR [rdi], esi
ret
J'utilise GCC 4.4.3
C'était publié en janvier 201 , il manque donc presque une décennie d'améliorations à l'optimiseur et aux messages d'erreur. La série gcc7 est stable depuis un certain temps. Attendez-vous à des optimisations manquées avec un compilateur aussi ancien, en particulier pour les jeux d'instructions modernes tels qu'AVX.
Après la compilation, chaque symbole n’est qu’une adresse et ne peut donc pas être un problème d’exécution.
De toute façon, tout symbole de membre est compilé en un décalage dans la classe actuelle, même si vous n'avez pas utilisé this
.
Lorsque name
est utilisé en C++, il peut s'agir de l'un des suivants.
::name
), dans l'espace de noms actuel ou dans l'espace de noms utilisé (lorsque using namespace ...
a été utilisé)Par conséquent, lorsque vous écrivez du code, le compilateur doit analyser chacun d'eux, de manière à rechercher le nom du symbole, du bloc actuel jusqu'à l'espace de noms global.
L'utilisation de this->name
aide le compilateur à restreindre la recherche de name
pour le rechercher uniquement dans l'étendue de la classe actuelle, ce qui signifie qu'il ignore les définitions locales et, s'il n'est pas trouvé dans l'étendue de la classe, ne le recherche pas dans la portée globale.
Voici un exemple simple indiquant comment "ceci" pourrait être utile pendant l'exécution:
#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;
}
Votre machine ne sait rien des méthodes de classe, ce sont des fonctions normales sous le capot. Par conséquent, les méthodes doivent être implémentées en passant toujours un pointeur sur l'objet en cours, il est simplement implicite en C++, c'est-à-dire que T Class::method(...)
n'est qu'un sucre syntaxique pour T Class_Method(Class* this, ...)
.
D'autres langages comme Python ou Lua choisissent de le rendre explicite et les API C orientées objet modernes telles que Vulkan (contrairement à OpenGL) utilisent un modèle similaire.
étant donné que je l'utilise habituellement à chaque fois, je fais référence à une variable membre ou à une fonction.
Vous toujours utilisez this
lorsque vous vous référez à une variable membre ou à une fonction. Il n'y a tout simplement pas d'autre moyen d'atteindre les membres. Le seul choix est implicite vs notation explicite.
Revenons à voir comment cela a été fait avant this
pour comprendre ce que this
est.
Sans OOP:
struct A {
int x;
};
void foo(A* that) {
bar(that->x)
}
Avec OOP mais en écrivant this
explicitement
struct A {
int x;
void foo(void) {
bar(this->x)
}
};
en utilisant une notation plus courte:
struct A {
int x;
void foo(void) {
bar(x)
}
};
Mais la différence ne concerne que le code source. Tous sont compilés à la même chose. Si vous créez une méthode membre, le compilateur créera un argument de pointeur et le nommera "this". Si vous omettez this->
lorsque vous vous référez à un membre, le compilateur est assez malin pour l'insérer, la plupart du temps. C'est ça. La seule différence est 6 lettres de moins dans la source.
Écrire this
est explicite lorsqu'il y a une ambiguïté, à savoir une autre variable nommée comme votre variable membre:
struct A {
int x;
A(int x) {
this->x = x
}
};
Il existe certains cas, comme __thiscall, où OO et le code non-OO peuvent se terminer de manière différente dans asm, mais chaque fois que le pointeur est passé sur la pile, puis optimisé. register ou dans ECX depuis le tout début ne le rend pas "pas un pointeur".
"this" peut également protéger contre l'observation par un paramètre de fonction, par exemple:
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;
}
(Évidemment, écrire un tel code est découragé.)
si le compilateur insère une fonction membre appelée avec une liaison statique plutôt que dynamique, il pourra peut-être optimiser le pointeur this
. Prenons cet exemple simple:
#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 avec le drapeau -march=x86-64 -O -S
est capable de compiler cout << e.foo()
en trois instructions:
movl $10, %esi
leaq _ZSt4cout(%rip), %rdi
call _ZNSolsEi@PLT
Ceci est un appel à std::ostream::operator<<
. Rappelez-vous que cout << e.foo();
est un sucre syntaxique pour std::ostream::operator<< (cout, e.foo());
. Et operator<<(int)
pourrait être écrit de deux manières: static operator<< (ostream&, int)
, en tant que fonction non membre, où l'opérande à gauche est un paramètre explicite, ou operator<<(int)
, en tant que fonction membre, où il est implicitement this
.
Le compilateur a pu déduire que e.foo()
sera toujours la constante 10
. Étant donné que la convention d'appel x86 64 bits consiste à transmettre les arguments de la fonction dans les registres, cette compilation est compilée selon l'instruction unique movl
, qui définit le second paramètre de fonction sur 10
. L'instruction leaq
définit le premier argument (qui peut être un ostream&
explicite ou le this
implicite) à &cout
. Ensuite, le programme crée un call
à la fonction.
Dans des cas plus complexes, cependant, par exemple si vous avez une fonction prenant un paramètre example&
, le compilateur doit rechercher this
, car this
indique au programme quelle instance. cela fonctionne avec, et par conséquent, avec quel membre de données x
l'instance à rechercher.
Considérons cet exemple:
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 fonction bar()
est compilée avec un peu de passe-partout et l’instruction:
movl (%rdi), %eax
ret
Vous vous souvenez de l'exemple précédent que %rdi
sur x86-64 est le premier argument de la fonction, le pointeur implicite this
de l'appel à e.foo()
. Le mettre entre parenthèses, (%rdi)
, signifie rechercher la variable à cet emplacement. (Étant donné que la seule donnée dans une instance example
est x
, &e.x
se trouve être identique à &e
dans ce cas.) Déplacer le contenu vers %eax
définit la valeur de retour.
Dans ce cas, le compilateur avait besoin de l'argument implicite this
de foo(/* example* this */)
pour pouvoir trouver &e
et donc &e.x
. En fait, dans une fonction membre (ce n’est pas static
), x
, this->x
et (*this).x
signifient la même chose.
this
est en effet un pointeur d'exécution (bien qu'un implicitement fourni par le compilateur), comme cela a été itéré dans la plupart des réponses. Il est utilisé pour indiquer sur quelle instance d'une classe une fonction membre donnée doit opérer lorsqu'elle est appelée. pour toute instance donnée c
de la classe C
, lorsqu'une fonction membre cf()
est appelée, c.cf()
reçoit un pointeur this
égal à &c
(ceci s'applique naturellement aussi à toute structure s
de type S
, lors de l'appel de la fonction membre s.sf()
, comme cela sera utilisé pour des démonstrations plus propres). Il peut même être qualifié cv comme n'importe quel autre pointeur, avec les mêmes effets (mais, malheureusement, pas la même syntaxe en raison de son caractère spécial); ceci est couramment utilisé pour la correction const
, et beaucoup moins fréquemment pour la correction 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";
Exemple de sortie:
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
Ces valeurs ne sont pas garanties et peuvent facilement changer d'une exécution à l'autre. ceci peut être facilement observé lors de la création et du test d'un programme, grâce à l'utilisation d'outils de compilation.
Mécaniquement, cela ressemble à un paramètre caché ajouté au début de la liste d'arguments de chaque fonction membre; x.f() cv
peut être vu comme une variante spéciale de f(cv X* this)
, mais avec un format différent pour des raisons linguistiques. En fait, il y avait des propositions récentes de Stroustrup et Sutter pour unifier la syntaxe d'appel de x.f(y)
et f(x, y)
, ce qui aurait fait de ce comportement implicite une règle linguistique explicite. Malheureusement, on s’est inquiété du fait que cela pourrait causer quelques surprises non souhaitées aux développeurs de bibliothèques, et donc pas encore implémenté; à ma connaissance, la proposition la plus récente est ne proposition conjointe, pour que f(x,y)
puisse se rabattre sur x.f(y)
si aucun f(x,y)
n'est trouvé , similaire à l'interaction entre, par exemple, std::begin(x)
et la fonction membre x.begin()
.
Dans ce cas, this
ressemblerait davantage à un pointeur normal et le programmeur pourrait le spécifier manuellement. Si une solution est trouvée pour permettre la forme la plus robuste sans violer le principe de moindre étonnement (ou laisser passer toute autre préoccupation), alors un équivalent de this
pourra également être généré implicitement comme un pointeur normal pour fonctions non-membres, ainsi.
De manière connexe, il est important de noter que this
est l'adresse de l'instance, telle qu'elle est vue par cette instance ; Bien que le pointeur lui-même soit une chose d'exécution, il n'a pas toujours la valeur que vous penseriez avoir. Cela devient pertinent lorsque vous examinez des classes avec des hiérarchies d'héritage plus complexes. En particulier, dans les cas où une ou plusieurs classes de base contenant des fonctions membres n'ont pas la même adresse que la classe dérivée elle-même. Trois cas en particulier viennent à l’esprit:
Notez que ceux-ci sont démontrés en utilisant MSVC, avec des mises en page de classes générées via le paramètre de compilateur ndocumented -d1reportSingleClassLayout , car je le trouve plus lisible que les équivalents GCC ou Clang.
Disposition non standard: Lorsqu'une classe est une disposition standard, l'adresse du premier membre de données d'une instance est exactement identique à l'adresse de l'instance elle-même; ainsi, this
peut être considéré comme équivalent à l'adresse du premier membre de données. Cela sera vrai même si ledit membre de données est un membre d'une classe de base, tant que la classe dérivée continue de suivre les règles de présentation standard. ... Inversement, cela signifie également que si la classe dérivée n'est pas en présentation standard, cela n'est plus garanti.
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";
Exemple de sortie:
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
Notez que StandardBase::address()
est fourni avec un pointeur this
différent de NonStandardDerived::address()
, même lorsqu'il est appelé sur la même instance. Cela est dû au fait que l'utilisation par le dernier d'une table vt a amené le compilateur à insérer un membre masqué.
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
Classes de base virtuelles: En raison des bases virtuelles qui suivent après la classe la plus dérivée, le pointeur this
fourni à une fonction membre héritée d'une fonction virtuelle base sera différente de celle fournie aux membres de la classe dérivée elle-même.
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";
Exemple de sortie:
Derived class with virtual base:
* v address: 0x0000008f8314f8b0
* v this pointer: 0x0000008f8314f8b0
* this pointer (as VBase): 0x0000008f8314f8b8
* this pointer (as VDerived): 0x0000008f8314f8b0
Une fois encore, la fonction membre de la classe de base est fournie avec un pointeur this
différent, en raison du fait que VDerived
ait hérité de VBase
avec une adresse de départ différente de VDerived
elle-même.
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
Héritage multiple: Comme on peut s'y attendre, l'héritage multiple peut facilement conduire à des cas où le pointeur this
passé à une fonction membre est différent de celui this
pointeur transmis à une fonction membre différente, même si les deux fonctions sont appelées avec la même instance. Cela peut arriver pour les fonctions membres de toute classe de base autre que la première, de la même manière que lorsque vous travaillez avec des classes de mise en page non standard (où toutes les classes de base après la première commencent à une adresse différente de celle de la classe dérivée elle-même) ... mais cela Cela peut être particulièrement surprenant dans le cas de virtual
fonctions, lorsque plusieurs membres fournissent des fonctions virtuelles avec la même signature.
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";
Exemple de sortie:
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)
Nous nous attendrions à ce que chaque raw_address()
ait les mêmes règles, chacune étant explicitement une fonction distincte, et donc que Base2::raw_address()
renverra une valeur différente de Derived::raw_address()
. Mais comme nous savons que les fonctions dérivées appelleront toujours la forme la plus dérivée, comment address()
est-il correct lorsqu'il est appelé à partir d'une référence à Base2
? Cela est dû à une petite astuce du compilateur appelée "ajusteur", qui est un assistant qui prend le pointeur this
d'une instance de classe de base et l'ajuste, si nécessaire, à la classe la plus dérivée.
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 vous êtes curieux, n'hésitez pas à bricoler ce petit programme , pour voir comment les adresses changent si vous l'exécutez plusieurs fois, ou dans les cas où il pourrait avoir une valeur différente de vous pouvez vous attendre.
this
est un pointeur. C'est comme un paramètre implicite qui fait partie de chaque méthode. Vous pouvez imaginer utiliser des fonctions C simples et écrire du code comme:
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 code similaire pourrait ressembler à ceci:
mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();