89 votes

Est-ce une bonne pratique de toujours utiliser des pointeurs intelligents ?

Je trouve que les pointeurs intelligents sont beaucoup plus confortables que les pointeurs bruts. Est-ce donc une bonne idée de toujours utiliser des pointeurs intelligents ? (Veuillez noter que je viens de Java et que je n'aime pas beaucoup l'idée d'une gestion explicite de la mémoire. Donc, à moins qu'il n'y ait de sérieux problèmes de performance avec les pointeurs intelligents, j'aimerais m'en tenir à eux. )

Note : Bien que je sois issu du monde Java, je comprends assez bien l'implémentation des pointeurs intelligents et les concepts de RAII. Vous pouvez donc considérer cette connaissance comme acquise de ma part lorsque vous postez une réponse. J'utilise l'allocation statique presque partout et je n'utilise les pointeurs que lorsque c'est nécessaire. Ma question est simplement la suivante : Puis-je toujours utiliser des pointeurs intelligents à la place des pointeurs bruts ?

92voto

Matthieu M. Points 101624

Compte tenu des nombreuses modifications apportées, j'ai l'impression qu'il serait utile de disposer d'un résumé complet.

1. Quand ne pas le faire

Il existe deux situations dans lesquelles vous ne devez pas utiliser de pointeurs intelligents.

La première situation est exactement la même que celle dans laquelle vous ne devez pas utiliser un C++ en fait. IE : DLL liée si vous n'offrez pas le code source au client. Disons que c'est anecdotique.

La seconde est beaucoup plus fréquente : un gestionnaire intelligent, c'est la propriété . Vous pouvez utiliser des pointeurs pour pointer sur des ressources existantes sans gérer leur durée de vie, par exemple :

void notowner(const std::string& name)
{
  Class* pointer(0);
  if (name == "cat")
    pointer = getCat();
  else if (name == "dog")
    pointer = getDog();

  if (pointer) doSomething(*pointer);
}

Cet exemple est soumis à des contraintes. Mais un pointeur est sémantiquement différent d'une référence en ce sens qu'il peut pointer vers un emplacement non valide (le pointeur nul). Dans ce cas, il est tout à fait possible de ne pas utiliser un pointeur intelligent à la place, car vous ne voulez pas gérer la durée de vie de l'objet.

2. Gestionnaires intelligents

À moins que vous n'écriviez une classe de gestionnaire intelligent, si vous utilisez le mot-clé delete vous faites quelque chose de mal.

C'est un point de vue controversé, mais après avoir examiné tant d'exemples de codes défectueux, je ne prends plus de risques. Donc, si vous écrivez new vous avez besoin d'un gestionnaire intelligent pour la mémoire nouvellement allouée. Et vous en avez besoin dès maintenant.

Cela ne signifie pas que vous êtes moins bon programmeur ! Au contraire, réutiliser un code qui a fait ses preuves au lieu de réinventer la roue encore et encore est une compétence clé.

Maintenant, la vraie difficulté commence : quel gestionnaire intelligent ?

3. Pointeurs intelligents

Il existe plusieurs pointeurs intelligents, avec des caractéristiques différentes.

Saut à la corde std::auto_ptr que vous devriez généralement éviter (sa sémantique de copie est foireuse).

  • scoped_ptr : pas de frais généraux, ne peut être copié ou déplacé.
  • unique_ptr : pas de frais généraux, ne peut être copié, peut être déplacé.
  • shared_ptr / weak_ptr Les données de l'enquête, qui peuvent être copiées, entraînent un certain surcoût (comptage des références).

En général, essayez d'utiliser soit scoped_ptr o unique_ptr . Si vous avez besoin de plusieurs propriétaires, essayez de changer le design. Si vous ne pouvez pas modifier la conception et que vous avez vraiment besoin de plusieurs propriétaires, utilisez un shared_ptr mais attention aux cycles de références qui doivent être interrompus à l'aide d'un weak_ptr quelque part au milieu.

4. Conteneurs intelligents

De nombreux pointeurs intelligents ne sont pas destinés à être copiés, ce qui compromet quelque peu leur utilisation avec les conteneurs STL.

Au lieu de recourir à shared_ptr et ses frais généraux, utiliser les conteneurs intelligents de la Conteneur de pointeurs Boost . Ils émulent l'interface des conteneurs STL classiques mais stockent les pointeurs qu'ils possèdent.

5. Rouler soi-même

Dans certains cas, vous pouvez souhaiter mettre en place votre propre gestionnaire intelligent. Vérifiez au préalable que vous n'avez pas manqué une fonctionnalité dans les bibliothèques que vous utilisez.

Il est assez difficile d'écrire un gestionnaire intelligent en présence d'exceptions. Vous ne pouvez généralement pas supposer que la mémoire est disponible ( new peut échouer) ou que Copy Constructor ont la no throw garantie.

Il peut être acceptable, dans une certaine mesure, d'ignorer la std::bad_alloc et imposer cette exception. Copy Constructor d'un certain nombre d'aides n'échouent pas... après tout, c'est ce qu'il faut faire. boost::shared_ptr fait pour son effaceur D paramètre du modèle.

Mais je ne le recommanderais pas, surtout pour un débutant. Il s'agit d'une question délicate, et vous n'êtes pas susceptible de remarquer les bogues dans l'immédiat.

6. Les exemples

// For the sake of short code, avoid in real code ;)
using namespace boost;

// Example classes
//   Yes, clone returns a raw pointer...
// it puts the burden on the caller as for how to wrap it
//   It is to obey the `Cloneable` concept as described in 
// the Boost Pointer Container library linked above
struct Cloneable
{
  virtual ~Cloneable() {}
  virtual Cloneable* clone() const = 0;
};

struct Derived: Cloneable
{
  virtual Derived* clone() const { new Derived(*this); }
};

void scoped()
{
  scoped_ptr<Cloneable> c(new Derived);
} // memory freed here

// illustration of the moved semantics
unique_ptr<Cloneable> unique()
{
  return unique_ptr<Cloneable>(new Derived);
}

void shared()
{
  shared_ptr<Cloneable> n1(new Derived);
  weak_ptr<Cloneable> w = n1;

  {
    shared_ptr<Cloneable> n2 = n1;          // copy

    n1.reset();

    assert(n1.get() == 0);
    assert(n2.get() != 0);
    assert(!w.expired() && w.get() != 0);
  } // n2 goes out of scope, the memory is released

  assert(w.expired()); // no object any longer
}

void container()
{
  ptr_vector<Cloneable> vec;
  vec.push_back(new Derived);
  vec.push_back(new Derived);

  vec.push_back(
    vec.front().clone()         // Interesting semantic, it is dereferenced!
  );
} // when vec goes out of scope, it clears up everything ;)

18voto

Pointeurs intelligents faire effectuent une gestion explicite de la mémoire, et si vous ne comprenez pas comment ils le font, vous vous exposez à un monde d'ennuis lorsque vous programmez avec C++. N'oubliez pas que la mémoire n'est pas la seule ressource qu'ils gèrent.

Mais pour répondre à votre question, vous devriez préférer les pointeurs intelligents comme première approximation d'une solution, tout en étant prêt à les abandonner si nécessaire. Vous ne devriez jamais utiliser de pointeurs (ou toute autre sorte) ou d'allocation dynamique lorsque cela peut être évité. Par exemple :

string * s1 = new string( "foo" );      // bad
string s2( "bar" );    // good

Editer : Pour répondre à votre question complémentaire "Puis-je toujours utiliser des pointeurs intelligents à la place des pointeurs bruts ? Alors, non, vous ne pouvez pas. Si (par exemple) vous devez implémenter votre propre version de l'opérateur new, vous devrez faire en sorte qu'il renvoie un pointeur, et non un pointeur intelligent.

14voto

sth Points 91594

En général, vous ne devriez pas utiliser de pointeurs (intelligents ou autres) si vous n'en avez pas besoin. Il est préférable de faire des variables locales, des membres de classe, des éléments vectoriels et des éléments similaires des objets normaux plutôt que des pointeurs sur des objets. (Puisque vous venez de Java, vous êtes probablement tenté d'allouer tout avec new ce qui n'est pas recommandé).

Cette approche (" RAII ") vous évite de vous préoccuper des pointeurs la plupart du temps.

L'utilisation de pointeurs dépend de la situation et de la raison exacte pour laquelle vous avez besoin de pointeurs, mais en général, des pointeurs intelligents peuvent être utilisés. Il se peut que ce ne soit pas toujours (en gras) est la meilleure option, mais cela dépend de la situation spécifique.

9voto

Steve Jessop Points 166970

Un bon moment no pour utiliser des pointeurs intelligents, se trouve à la limite de l'interface d'une DLL. Vous ne savez pas si d'autres exécutables seront construits avec le même compilateur/les mêmes bibliothèques. La convention d'appel DLL de votre système ne spécifie pas à quoi ressemblent les classes standard ou TR1, y compris les pointeurs intelligents.

Au sein d'un exécutable ou d'une bibliothèque, si vous souhaitez représenter la propriété de la pointe, les pointeurs intelligents sont en moyenne la meilleure façon de le faire. Il est donc normal de vouloir toujours les utiliser de préférence aux pointeurs bruts. Quant à savoir si vous pouvez toujours les utiliser, c'est une autre question.

Pour un exemple concret de ce qu'il ne faut pas faire, supposons que vous écriviez une représentation d'un graphe générique dont les sommets sont représentés par des objets et les arêtes par des pointeurs entre les objets. Les pointeurs intelligents habituels ne vous seront d'aucune utilité : les graphes peuvent être cycliques et aucun nœud particulier ne peut être tenu responsable de la gestion de la mémoire des autres nœuds, de sorte que les pointeurs partagés et faibles sont insuffisants. Vous pouvez par exemple tout placer dans un vecteur et utiliser des indices au lieu de pointeurs, ou tout placer dans un deque et utiliser des pointeurs bruts. Vous pouvez utiliser shared_ptr si vous le souhaitez, mais cela n'ajoutera rien d'autre que des frais généraux. Vous pouvez également opter pour un GC à balayage de marques.

Un cas plus marginal : Je préfère que les fonctions prennent un paramètre par pointeur ou par référence, et promet de ne pas conserver de pointeur ou de référence à celui-ci plutôt que de prendre un shared_ptr et vous laissent vous demander s'ils ne conservent pas une référence après leur retour, si vous ne risquez pas de casser quelque chose si vous modifiez encore une fois la référence, etc. Le fait de ne pas conserver les références est une chose qui n'est souvent pas documentée de manière explicite, mais qui va de soi. Peut-être que cela ne devrait pas aller de soi, mais c'est le cas. Les pointeurs intelligents impliquent quelque chose à propos de la propriété, et le fait de faussement impliquer cela peut être déroutant. Ainsi, si votre fonction prend un shared_ptr Il faut donc veiller à indiquer s'il peut conserver une référence ou non.

6voto

Mark Wilkins Points 29291

Dans de nombreuses situations, je pense qu'ils sont définitivement la meilleure solution (moins de code de nettoyage désordonné, moins de risques de fuites, etc.) ). Cependant, il y a un léger surcoût. Si j'écrivais un code qui devait être aussi rapide que possible (disons une boucle serrée qui doit faire une allocation et une libération), je n'utiliserais probablement pas de pointeur intelligent dans l'espoir de gagner un peu de vitesse. Mais je doute que cela fasse une différence mesurable dans la plupart des situations.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X