web-dev-qa-db-fra.com

Comment passer std :: unique_ptr autour?

J'ai ma première tentative d'utilisation de C++ 11 unique_ptr; Je remplace un pointeur brut polymorphe dans un de mes projets, qui appartient à une classe, mais qui est passé assez souvent.

J'avais l'habitude d'avoir des fonctions comme:

bool func(BaseClass* ptr, int other_arg) {
  bool val;
  // plain ordinary function that does something...
  return val;
}

Mais je me suis vite rendu compte que je ne pourrais pas passer à:

bool func(std::unique_ptr<BaseClass> ptr, int other_arg);

Parce que l'appelant devrait gérer la propriété du pointeur sur la fonction, ce que je ne veux pas. Alors, quelle est la meilleure solution à mon problème?

J'ai pensé à passer le pointeur comme référence, comme ceci:

bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);

Mais je me sens très mal à l'aise de le faire, tout d'abord parce qu'il semble non instinctif de passer quelque chose déjà tapé comme _ptr comme référence, quelle serait la référence d'une référence. Deuxièmement parce que la signature de la fonction devient encore plus grande. Troisièmement, parce que dans le code généré, il faudrait deux indirections de pointeur consécutives pour atteindre ma variable.

67
lvella

Si vous souhaitez que la fonction utilise la pointe, passez-y une référence. Il n'y a aucune raison de lier la fonction pour qu'elle ne fonctionne qu'avec une sorte de pointeur intelligent:

bool func(BaseClass& base, int other_arg);

Et sur le site d'appel, utilisez operator*:

func(*some_unique_ptr, 42);

Alternativement, si l'argument base peut être nul, conservez la signature telle quelle et utilisez la fonction membre get():

bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);
82

L'avantage d'utiliser std::unique_ptr<T> (En plus de ne pas avoir à se rappeler d'appeler explicitement delete ou delete[]) Est qu'il garantit qu'un pointeur est nullptr ou il pointe vers une instance valide de l'objet (de base). J'y reviendrai après avoir répondu à votre question, mais le premier message est DO utilisez des pointeurs intelligents pour gérer la durée de vie des objets alloués dynamiquement.

Maintenant, votre problème est en fait comment l'utiliser avec votre ancien code .

Ma suggestion est que si vous ne voulez pas transférer ou partager la propriété, vous devez toujours transmettre des références à l'objet. Déclarez votre fonction comme ceci (avec ou sans qualificatifs const, selon les besoins):

bool func(BaseClass& ref, int other_arg) { ... }

Ensuite, l'appelant, qui a un std::shared_ptr<BaseClass> ptr Traitera le cas nullptr ou demandera à bool func(...) de calculer le résultat:

if (ptr) {
  result = func(*ptr, some_int);
} else {
  /* the object was, for some reason, either not created or destroyed */
}

Cela signifie que tout appelant doit promettre que la référence est valide et qu'elle restera valide tout au long de l'exécution du corps de la fonction.


Voici la raison pour laquelle je crois fermement que vous devriez pas passer des pointeurs bruts ou des références à des pointeurs intelligents.

Un pointeur brut n'est qu'une adresse mémoire. Peut avoir une (au moins) 4 significations:

  1. L'adresse d'un bloc de mémoire où se trouve l'objet souhaité. (le bien)
  2. L'adresse 0x0 dont vous pouvez être certain n'est pas déréférençable et peut avoir la sémantique de "rien" ou "pas d'objet". (le mauvais)
  3. L'adresse d'un bloc de mémoire qui se trouve en dehors de l'espace adressable de votre processus (son déréférencement provoquera, espérons-le, le plantage de votre programme). (le laid)
  4. L'adresse d'un bloc de mémoire qui peut être déréférencé mais qui ne contient pas ce que vous attendez. Peut-être que le pointeur a été accidentellement modifié et qu'il pointe maintenant vers une autre adresse accessible en écriture (d'une variable complètement différente dans votre processus). L'écriture dans cet emplacement de mémoire provoquera parfois beaucoup de plaisir pendant l'exécution, car le système d'exploitation ne se plaindra pas tant que vous serez autorisé à y écrire. (Zoinks!)

L'utilisation correcte des pointeurs intelligents atténue les cas plutôt effrayants 3 et 4, qui ne sont généralement pas détectables au moment de la compilation et que vous ne rencontrez généralement qu'au moment de l'exécution lorsque votre programme plante ou fait des choses inattendues.

Passer des pointeurs intelligents comme arguments présente deux inconvénients: vous ne pouvez pas modifier le const- ness de l'objet pointé sans faire de copie (ce qui ajoute surcharge pour shared_ptr et n'est pas possible pour unique_ptr), et il vous reste la deuxième signification (nullptr).

J'ai marqué le deuxième cas comme (le mauvais) du point de vue de la conception. Il s'agit d'un argument plus subtil sur la responsabilité.

Imaginez ce que cela signifie lorsqu'une fonction reçoit un nullptr comme paramètre. Il doit d'abord décider quoi en faire: utiliser une valeur "magique" à la place de l'objet manquant? changer complètement de comportement et calculer autre chose (qui ne nécessite pas l'objet)? paniquer et lever une exception? De plus, que se passe-t-il lorsque la fonction prend 2, 3 ou même plus d'arguments par pointeur brut? Il doit vérifier chacun d'eux et adapter son comportement en conséquence. Cela ajoute un tout nouveau niveau en plus de la validation des entrées sans raison réelle.

L'appelant doit être celui qui a suffisamment d'informations contextuelles pour prendre ces décisions, ou, en d'autres termes, le mauvais est moins effrayant, plus vous en savez. La fonction, d'autre part, devrait simplement tenir la promesse de l'appelant que la mémoire vers laquelle elle est pointée est sûre de fonctionner comme prévu. (Les références sont toujours des adresses de mémoire, mais représentent conceptuellement une promesse de validité.)

25
Victor Savu

Je suis d'accord avec Martinho, mais je pense qu'il est important de souligner la sémantique de propriété d'un pass-by-reference. Je pense que la bonne solution consiste à utiliser un simple passage par référence ici:

bool func(BaseClass& base, int other_arg);

La signification communément acceptée d'un passage par référence en C++ est comme si l'appelant de la fonction disait à la fonction "ici, vous pouvez emprunter cet objet, l'utiliser et le modifier (sinon const), mais uniquement pour le durée du corps de fonction. " Ceci n'est en aucun cas en conflit avec les règles de propriété du unique_ptr parce que l'objet est simplement emprunté pour une courte période, il n'y a pas de transfert de propriété réel (si vous prêtez votre voiture à quelqu'un, lui signez-vous le titre?).

Ainsi, même si cela peut sembler mauvais (au niveau de la conception, des pratiques de codage, etc.) de retirer la référence (ou même le pointeur brut) du unique_ptr, ce n'est pas le cas car il est parfaitement conforme aux règles de propriété fixées par le unique_ptr. Et puis, bien sûr, il y a d'autres avantages de Nice, comme la syntaxe propre, pas de restriction aux seuls objets appartenant à un unique_ptr, et donc.

21
Mikael Persson

Personnellement, j'évite de tirer une référence d'un pointeur/pointeur intelligent. Car que se passe-t-il si le pointeur est nullptr? Si vous changez la signature en ceci:

bool func(BaseClass& base, int other_arg);

Vous devrez peut-être protéger votre code contre les déréférences de pointeur nul:

if (the_unique_ptr)
  func(*the_unique_ptr, 10);

Si la classe est le seul propriétaire du pointeur, la seconde alternative de Martinho semble plus raisonnable:

func(the_unique_ptr.get(), 10);

Vous pouvez également utiliser std::shared_ptr. Cependant, s'il existe une seule entité responsable de delete, le std::shared_ptr les frais généraux ne sont pas payants.

0
Guilherme Ferreira