Comptage automatique des références dans C++

De RAD Studio
Aller à : navigation, rechercher

Remonter à Considérations C++ pour les applications multi-périphériques


Dans C++, les objets de style Delphi (objets de types dérivés de TObject) sont alloués hors du tas (via new) et libérés via un appel à delete. RAD Studio a changé la gestion de la durée de vie de ces objets dans les plates-formes mobiles en basculant sur le comptage automatique de références (ARC, Automatic Reference Counting).

Si vous n'êtes pas familiers avec l'ARC, veuillez lire la page suivante : Comptage automatique de références dans les compilateurs mobiles Delphi.

Principes fondamentaux de ARC

Les concepts clés de l'ARC pour les pointeurs de classes de style Delphi sont décrits ci-dessous :

  • La durée de vie de chaque objet est gérée via un RefCount (similaire à la façon dont TInterfacedObject est géré sur du code non-ARC)
  • Chaque pointeur ajoute un compteur de références à l'instance (pour les pointeurs réguliers, traités comme des pointeurs forts)
  • L'invocation de delete sur un pointeur de classe de style Delphi décrémente simplement le RefCount de l'instance et efface le pointeur
  • L'affectation de zéro à un pointeur de classe de style Delphi décrémente le RefCount de l'instance et efface le pointeur
  • La pseudo-destruction d'un pointeur de classe de style Delphi décrémente le RefCount de l'instance et efface le pointeur
  • Une fonction renvoyant un pointeur de classe de style Delphi incrémente le RefCount
  • L'appelant d'une fonction renvoyant un pointeur de classe de style Delphi décrémente le RefCount du pointeur renvoyé à la fin de l'expression complète (la façon dont C++ gère les temporaires renvoyés).

Exemples ARC

Les extraits de code ci-après montrent les différences fondamentales entre le code ARC (Mobile) et le code non-ARC (Bureau). Les extraits de code ci-dessous utilisent TObject mais le même concept s'applique à chaque classe de style Delphi (telle que TButton, TStringList, TForm, etc).

Pour illustrer comment les pointeurs de classes de style Delphi renvoyés sont gérés, utilisez la fonction GetObject  :

C++:

TObject* GetObject() {
	return new TObject();
}
Exemple de code Remarques
void test1() {
  TObject* p = GetObject();
  ShowMessage(System::Sysutils::IntToStr(p->RefCount));
}

Non-ARC : En mode non-ARC, ce code entraîne une fuite, puisque l'instance n'a pas été supprimée.

ARC : Avec ARC, quand p est assigné à la valeur renvoyée par GetObject, le RefCount de l'instance est incrémenté.

Quand p sort de la portée, la propriété RefCount est décrémentée et l'instance est libérée.

void test2() {
  TObject* p = GetObject();
  delete p;
}

Non-ARC : L'instance est nettoyée correctement.

ARC : Avec ARC, la propriété RefCount de p est incrémentée (lors de l'affectation à p d'un pointeur de classe de style Delphi renvoyé)

et décrémentée (lors de l'appel à delete). L'appel à delete initialisera également à zéro la valeur de p. Ceci est requis afin que le code généré par le compilateur,

qui décrémente son RefCount en sortie de portée, n'effectue pas une double libération de l'instance.

void test3() {
  TObject* p = GetObject();
  p = 0;
}

Non-ARC : Cela entraîne une fuite sur le bureau. La mise à zéro d'un pointeur efface simplement la variable. Elle ne détruit pas l'instance.

ARC : Avec ARC, l'instance est correctement nettoyée, puisque la mise à zéro d'un pointeur de classe de style Delphi est identique à l'invocation de delete :

elle décrémente le RefCount de l'instance.

void test4(bool flag) {
  TObject* outer;
  if (flag) {
	TObject* p1 = new TObject(); //p1's RefCount is 1
	// ...
	outer = p1; // increments RefCount
                    //(p1->RefCount=outer->RefCount=2)
	// ...
	delete p1; // (A) decrements the RefCount (outer->RefCount = 1)
	// ...
  }
  // (B)
}

Non-ARC : A (A) l'instance est détruite et outer est un pointeur non résolu/extravagant sur un objet invalide.

ARC : Avec ARC, l'instance n'est pas détruite à (A) puisque outer a un RefCount en suspens. L'instance est détruite à (B)

quand outer est nettoyé.

int test5() {
  return GetObject()->RefCount;
}

Non-ARC : Sans ARC, cet exemple perd l'objet renvoyé (sur Bureau ARC, TObject n'a pas de propriété RefCount).

ARC : Avec ARC, l'instance renvoyée est nettoyée.

int test6() {
  return std::auto_ptr<TObject>(GetObject())->RefCount;
}

Non-ARC : Cet exemple ne perd rien quand ARC est activé.

ARC : Quand ARC est activé, le destructeur du pointeur intelligent nettoie correctement l'instance renvoyée.

Pointeurs __weak et __unsafe

Les pointeurs sur des classes de style Delphi sont traités comme des références fortes par défaut. Vous pouvez également déclarer des pointeurs faibles et non protégés, respectivement avec __weak et __unsafe. Les pointeurs faibles doivent être utilisés afin d'éviter les références circulaires (cycliques). Par exemple, un objet parent peut avoir des références fortes sur tous ses enfants, et le dernier peut avoir un pointeur faible sur le parent.

class A : public TObject {  //Parent
public:
	B* b;
	~A();
};
A::~A() {
}

class B : public TObject { //Child
public:
	__weak A* a;
	~B();
};
B::~B() {
}

Les pointeurs Weak sont gérés par le runtime de telle sorte qu'ils valent NULL quand l'objet fort auquel ils font référence est détruit.

Les pointeurs Unsafe sont des pointeurs bruts non gérés. Il est important de noter que les __closures sont gérés comme des pointeurs faibles. En d'autres termes, la partie données du closure est effacée quand l'objet fort sous-jacent est détruit.

Meilleures pratiques

  • La règle d'or consistant à ne pas utiliser des pointeurs bruts pour la gestion des ressources est vraie même pour les pointeurs de style de classe Delphi ARC. N'invoquez pas le mécanisme ARC pour gérer une ressource, utilisez plutôt le template auto_ptr.
  • Les pointeurs zéro deviendront des pointeurs non résolus dans un environnement non-ARC. Vous pouvez utiliser la méthode TObject.DisposeOf pour forcer l'exécution d'un destructeur quand la présence de pointeurs non résolus ou de références intentionnelles à une instance n'est pas évidente. Mais chaque fois que c'est possible, utilisez un wrapper RAII et évitez de gérer les ressources via des pointeurs bruts, avec ou sans support ARC.

Voir aussi