web-dev-qa-db-fra.com

Quand dois-je utiliser des pointeurs bruts sur des pointeurs intelligents?

Après avoir lu cette réponse , il semble que c'est une meilleure pratique d'utiliser pointeurs intelligents autant que possible, et de réduire au minimum l'utilisation de pointeurs "normaux"/bruts .

Est-ce vrai?

55
Alon Gubkin

Non ce n'est pas vrai. Si une fonction a besoin d'un pointeur et n'a rien à voir avec la propriété, je crois fermement qu'un pointeur normal doit être transmis pour les raisons suivantes:

  • Pas de propriété, donc vous ne savez pas quel type de pointeur intelligent passer
  • Si vous passez un pointeur spécifique, comme shared_ptr, alors vous ne pourrez pas passer, disons, scoped_ptr

La règle serait la suivante - si vous savez qu'une entité doit prendre un certain type de propriété de l'objet, toujours use pointeurs intelligents - celui qui vous donne le type de propriété dont vous avez besoin. S'il n'y a pas de notion de propriété, jamais utilisez des pointeurs intelligents.

Exemple 1:

void PrintObject(shared_ptr<const Object> po) //bad
{
    if(po)
      po->Print();
    else
      log_error();
}

void PrintObject(const Object* po) //good
{
    if(po)
      po->Print();
    else
      log_error();
}

Exemple2:

Object* createObject() //bad
{
    return new Object;
}

some_smart_ptr<Object> createObject() //good
{
   return some_smart_ptr<Object>(new Object);
}
80
Armen Tsirunyan

L'utilisation de pointeurs intelligents pour gérer la propriété est la bonne chose à faire. Inversement, l'utilisation de pointeurs bruts partout où la propriété n'est pas un problème est pas incorrecte.

Voici quelques utilisations parfaitement légitimes des pointeurs bruts (rappelez-vous, on suppose toujours qu'ils ne sont pas propriétaires):

où ils rivalisent avec les références

  • passage d'argument; mais les références ne peuvent pas être nulles, elles sont donc préférables
  • en tant que membres de la classe, pour désigner l'association plutôt que la composition; généralement préférable aux références car la sémantique d'affectation est plus simple et en outre un invariant mis en place par les constructeurs peut garantir qu'ils ne sont pas 0 pour la durée de vie de l'objet
  • comme poignée d'un objet (éventuellement polymorphe) appartenant à un autre endroit; les références ne peuvent pas être nulles donc encore une fois elles sont préférables
  • std::bind Utilise une convention où les arguments passés sont copiés dans le foncteur résultant; cependant std::bind(&T::some_member, this, ...) ne fait qu'une copie du pointeur tandis que std::bind(&T::some_member, *this, ...) copie l'objet; std::bind(&T::some_member, std::ref(*this), ...) est une alternative

où ils le font pas en concurrence avec les références

  • comme itérateurs!
  • passage d'arguments des paramètres facultatif; ici, ils rivalisent avec boost::optional<T&>
  • comme poignée vers un objet (éventuellement polymorphe) appartenant à un autre endroit, quand ils ne peuvent pas être déclarés sur le site d'initialisation; encore une fois, en concurrence avec boost::optional<T&>

Pour rappel, il est presque toujours erroné d'écrire une fonction (qui n'est pas un constructeur ou un membre de fonction qui, par exemple, prend possession) qui accepte un pointeur intelligent à moins qu'il ne le transmette à son tour à un constructeur (par exemple, il est correct pour std::async Car sémantiquement, il est proche d'être un appel au constructeur std::thread). S'il est synchrone, pas besoin de pointeur intelligent.


Pour récapituler, voici un extrait qui illustre plusieurs des utilisations ci-dessus. Nous écrivons et utilisons une classe qui applique un foncteur à chaque élément d'un std::vector<int> Tout en écrivant une sortie.

class apply_and_log {
public:
    // C++03 exception: it's acceptable to pass by pointer to const
    // to avoid apply_and_log(std::cout, std::vector<int>())
    // notice that our pointer would be left dangling after call to constructor
    // this still adds a requirement on the caller that v != 0 or that we throw on 0
    apply_and_log(std::ostream& os, std::vector<int> const* v)
        : log(&os)
        , data(v)
    {}

    // C++0x alternative
    // also usable for C++03 with requirement on v
    apply_and_log(std::ostream& os, std::vector<int> const& v)
        : log(&os)
        , data(&v)
    {}
    // now apply_and_log(std::cout, std::vector<int> {}) is invalid in C++0x
    // && is also acceptable instead of const&&
    apply_and_log(std::ostream& os, std::vector<int> const&&) = delete;

    // Notice that without effort copy (also move), assignment and destruction
    // are correct.
    // Class invariants: member pointers are never 0.
    // Requirements on construction: the passed stream and vector must outlive *this

    typedef std::function<void(std::vector<int> const&)> callback_type;

    // optional callback
    // alternative: boost::optional<callback_type&>
    void
    do_work(callback_type* callback)
    {
        // for convenience
        auto& v = *data;

        // using raw pointers as iterators
        int* begin = &v[0];
        int* end = begin + v.size();
        // ...

        if(callback) {
            callback(v);
        }
    }

private:
    // association: we use a pointer
    // notice that the type is polymorphic and non-copyable,
    // so composition is not a reasonable option
    std::ostream* log;

    // association: we use a pointer to const
    // contrived example for the constructors
    std::vector<int> const* data;
};
14
Luc Danton

L'utilisation de pointeurs intelligents est toujours recommandée car ils documentent clairement la propriété.

Ce qui nous manque vraiment, cependant, est un pointeur intelligent "vierge", qui n'implique aucune notion de propriété.

template <typename T>
class ptr // thanks to Martinho for the name suggestion :)
{
public:
  ptr(T* p): _p(p) {}
  template <typename U> ptr(U* p): _p(p) {}
  template <typename SP> ptr(SP const& sp): _p(sp.get()) {}

  T& operator*() const { assert(_p); return *_p; }
  T* operator->() const { assert(_p); return _p; }

private:
  T* _p;
}; // class ptr<T>

C'est, en effet, la version la plus simple de tout pointeur intelligent qui puisse exister: un type qui documente qu'il ne possède pas la ressource qu'il pointe également.

6
Matthieu M.

Un exemple où le comptage de références (utilisé par shared_ptr en particulier) se décompose est lorsque vous créez un cycle à partir des pointeurs (par exemple, A pointe vers B, B pointe vers A ou A-> B-> C-> A, ou etc). Dans ce cas, aucun des objets ne sera jamais automatiquement libéré, car ils conservent tous un décompte de références supérieur à zéro.

Pour cette raison, chaque fois que je crée des objets qui ont une relation parent-enfant (par exemple une arborescence d'objets), j'utiliserai shared_ptrs dans les objets parents pour contenir leurs objets enfants, mais si les objets enfants ont besoin d'un pointeur vers leur parent , Je vais utiliser un simple pointeur C/C++ pour cela.

4
Jeremy Friesner

Je pense qu'une réponse un peu plus approfondie a été donnée ici: Quel type de pointeur dois-je utiliser quand?

Extrait de ce lien: "Utilisez des pointeurs stupides (pointeurs bruts) ou des références pour des références non propriétaires vers des ressources et lorsque vous savez que le la ressource survivra à l'objet/portée de référence. " (gras conservé de l'original)

Le problème est que si vous écrivez du code à usage général, il n'est pas toujours facile d'être absolument certain que l'objet survivra au pointeur brut. Considérez cet exemple:

struct employee_t {
    employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
    std::string m_first_name;
    std::string m_last_name;
};

void replace_current_employees_with(const employee_t* p_new_employee, std::list<employee_t>& employee_list) {
    employee_list.clear();
    employee_list.Push_back(*p_new_employee);
}

void main(int argc, char* argv[]) {
    std::list<employee_t> current_employee_list;
    current_employee_list.Push_back(employee_t("John", "Smith"));
    current_employee_list.Push_back(employee_t("Julie", "Jones"));
    employee_t* p_person_who_convinces_boss_to_rehire_him = &(current_employee_list.front());

    replace_current_employees_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

À sa grande surprise, la fonction replace_current_employees_with() peut par inadvertance entraîner la désallocation de l'un de ses paramètres avant qu'il n'ait fini de l'utiliser.

Ainsi, même si au premier abord il peut sembler que la fonction replace_current_employees_with() n'a pas besoin de s'approprier ses paramètres, elle a besoin d'une sorte de défense contre la possibilité que ses paramètres soient insidieusement désalloués avant d'avoir fini de les utiliser. La solution la plus simple consiste à s'approprier (partager temporairement) le ou les paramètres, probablement via un shared_ptr.

Mais si vous ne voulez vraiment pas vous approprier, il existe maintenant une option sûre - et c'est la partie plug sans vergogne de la réponse - " pointeurs enregistrés ". Les "pointeurs enregistrés" sont des pointeurs intelligents qui se comportent comme des pointeurs bruts, sauf qu'ils sont (automatiquement) définis sur null_ptr lorsque l'objet cible est détruit et, par défaut, lèvera une exception si vous essayez d'accéder à un objet qui a déjà été supprimé.

Notez également que les pointeurs enregistrés peuvent être "désactivés" (remplacés automatiquement par leur homologue de pointeur brut) par une directive de compilation, ce qui leur permet d'être utilisés (et entraînent des frais généraux) en mode débogage/test/bêta uniquement. Donc, vous devriez vraiment avoir recours à des pointeurs bruts réels assez rarement.

1
Noah

Quelques cas, où vous voudrez peut-être utiliser des pointeurs:

  • Pointeurs de fonction (évidemment pas de pointeur intelligent)
  • Définition de votre propre pointeur intelligent ou conteneur
  • Gérer la programmation de bas niveau, où les pointeurs bruts sont cruciaux
  • Décomposition des tableaux bruts
1
iammilind