web-dev-qa-db-fra.com

Utilisation de observer_ptr

Quel est exactement le point de la construction std::observer_ptr dans la spécification technique des fondamentaux de la bibliothèque V2?

Il me semble que tout ce qu'il fait est d'envelopper un _ T*, ce qui semble être une étape superflue s'il n'ajoute aucune sécurité de mémoire dynamique.

Dans tout mon code, j'utilise std::unique_ptr où je dois prendre la propriété explicite d'un objet et std::shared_ptr où je peux partager la propriété d'un objet.

Cela fonctionne très bien et empêche le déréférencement accidentel d'un objet déjà détruit.

std::observer_ptr n'offre aucune garantie quant à la durée de vie de l'objet observé, bien entendu.

S'il devait être construit à partir d'un std::unique_ptr ou std::shared_ptr Je verrais une utilisation dans une telle structure, mais tout code utilisant simplement T* va probablement continuer à le faire et s'ils envisagent de passer à autre chose, ce serait std::shared_ptr et/ou std::unique_ptr (selon l'utilisation).


Étant donné un exemple de fonction simple:

template<typename T>
auto func(std::observer_ptr<T> ptr){}

Où cela serait utile s'il empêchait les pointeurs intelligents de détruire leur objet stocké pendant leur observation.

Mais si je veux observer un std::shared_ptr ou std::unique_ptr Je dois ecrire:

auto main() -> int{
    auto uptr = std::make_unique<int>(5);
    auto sptr = std::make_shared<int>(6);
    func(uptr.get());
    func(sptr.get());
}

Ce qui ne le rend pas plus sûr que:

template<typename T>
auto func(T *ptr){}

Alors, à quoi sert cette nouvelle structure?

Est-ce uniquement pour une source auto-documentée?

43
CoffeeandCode

La proposition montre assez clairement que c'est juste pour l'auto-documentation:

Ce document propose observer_ptr, un type de pointeur intelligent (pas très) qui n'assume aucune responsabilité pour ses pointes, c'est-à-dire pour les objets qu'il observe. En tant que tel, il est destiné à remplacer presque directement les types de pointeurs bruts, avec l'avantage qu'en tant que type de vocabulaire, il indique son utilisation prévue sans besoin d'une analyse détaillée par les lecteurs de code.

35
Barry

Lorsque vous avez besoin d'un accès partagé mais pas d'une propriété partagée .

Le problème est que les pointeurs bruts sont toujours très utiles et ont des scénarios d'utilisation parfaitement respectables.

Lorsqu'un pointeur brut est géré par un pointeur intelligent son nettoyage est garanti et ainsi, pendant la durée de vie du pointeur intelligent , il est logique d'accéder aux données réelles via le pointeur brut que le pointeur intelligent gère.

Donc, lorsque nous créons des fonctions, cela prendrait normalement un pointeur brut, une bonne façon de promettre que la fonction ne supprimera pas ce pointeur est d'utiliser une classe fortement typée comme std::observer_ptr.

Lors du passage d'un pointeur brut géré comme argument à un std::observer_ptr paramètre de fonction, nous savons que la fonction ne va pas delete.

C'est une façon pour une fonction de dire "donnez-moi votre pointeur, je ne me mêlerai pas de son allocation, je vais juste l'utiliser pour observer".

Soit dit en passant, je ne suis pas intéressé par le nom std::observer_ptr car cela implique que vous pouvez regarder mais pas toucher. Mais ce n'est pas vraiment vrai. Je serais allé avec quelque chose de plus comme access_ptr.

Remarque supplémentaire:

Il s'agit d'un cas d'utilisation différent d'un std::shared_ptr. Le std::shared_ptr concerne le partage de la propriété et il doit niquement être utilisé lorsque vous ne pouvez pas déterminer laquelle L'objet propriétaire sortira d'abord de la portée.

Le std::observer_ptr, d'autre part, est pour quand vous voulez partager l'accès mais pas la propriété .

Il n'est pas vraiment approprié d'utiliser std::shared_ptr simplement pour partager l'accès car cela pourrait être très inefficace.

Donc, si vous gérez votre pointeur cible en utilisant un std::unique_ptr ou un std::shared_ptr il y a encore un cas d'utilisation pour pointeurs bruts et donc le rationnel pour un std::observer_ptr.

30
Galik

Est-ce juste pour l'auto-documentation source?

Oui.

20
user541686

Il semble d'après proposition que std::observer_ptr sert principalement à documenter qu'un pointeur est une référence non propriétaire vers un objet, plutôt qu'une référence propriétaire, tableau, chaîne ou itérateur.

Cependant, il y a quelques autres avantages à utiliser observer_ptr<T> plus de T*:

  1. Une construction par défaut observer_ptr sera toujours initialisé à nullptr; un pointeur normal peut ou non être initialisé, selon le contexte.
  2. observer_ptr ne supporte que les opérations qui ont du sens pour a référence; cela impose une utilisation correcte:
    • operator[] n'est pas implémenté pour observer_ptr, car il s'agit d'une opération array.
    • L'arithmétique du pointeur n'est pas possible avec observer_ptr, car ce sont des opérations itérateur.
  3. Deux observer_ptrs ont ordre faible strict sur toutes les implémentations, ce qui n'est pas garanti pour deux pointeurs arbitraires. Ceci est dû au fait operator< est implémenté en termes de std::less pour observer_ptr (comme avec std::unique_ptr et std::shared_ptr ).
  4. observer_ptr<void> ne semble pas être pris en charge, ce qui peut encourager l'utilisation de solutions plus sûres (par exemple std::any et std::variant )
16
Joseph Thomson

Une belle conséquence de l'utilisation de std::observer_ptr par rapport aux pointeurs bruts est qu'il fournit une meilleure alternative à la syntaxe d'instanciation de pointeurs multiples déroutante et sujette aux erreurs héritée de C.

std::observer_ptr<int> a, b, c;

est une amélioration

int *a, *b, *c;

ce qui est légèrement étrange du point de vue C++ et peut facilement être mal orthographié comme

int* a, b, c;
5
Richard Forrest

Oui, le point de std::observer_ptr Est en grande partie juste "auto-documentation" et c'est une fin valide en soi. Mais il convient de souligner que sans doute cela ne fait pas un excellent travail car il n'est pas évident exactement ce qu'est un pointeur "observateur". Tout d'abord, comme le souligne Galik, pour certains, le nom semble impliquer un engagement à ne pas modifier la cible, ce qui n'est pas l'intention, donc un nom comme access_ptr Serait mieux. Et deuxièmement, sans aucun qualificatif, le nom impliquerait une approbation de son comportement "non fonctionnel". Par exemple, on pourrait considérer un std::weak_ptr Comme un type de pointeur "observateur". Mais std::weak_ptr Convient au cas où le pointeur survit à l'objet cible en fournissant un mécanisme qui permet aux tentatives d'accès à l'objet (désalloué) d'échouer en toute sécurité. L'implémentation de std::observer_ptr Ne prend pas en charge ce cas. Donc, raw_access_ptr Serait peut-être un meilleur nom car il indiquerait mieux son défaut fonctionnel.

Donc, comme vous le demandez à juste titre, quel est l'intérêt de ce pointeur "non propriétaire" fonctionnellement contesté? La raison principale est probablement la performance. De nombreux programmeurs C++ perçoivent la surcharge d'un std::share_ptr Comme étant trop élevée et n'utilisent donc que des pointeurs bruts lorsqu'ils ont besoin de pointeurs "observateurs". Le std::observer_ptr Proposé tente d'apporter une petite amélioration de la clarté du code à un coût de performance acceptable. Plus précisément, aucun coût de performance.

Malheureusement, il semble y avoir un optimisme répandu mais, à mon avis, irréaliste quant à la sécurité d'utilisation des pointeurs bruts comme pointeurs "observateurs". En particulier, bien qu'il soit facile d'énoncer une exigence selon laquelle l'objet cible doit survivre au std::observer_ptr, Il n'est pas toujours facile d'être absolument certain qu'il est satisfait. 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_last_employee_with(const std::observer_ptr<employee_t> p_new_employee, std::list<employee_t>& employee_list) {
    if (1 <= employee_list.size()) {
        employee_list.pop_back();
    }
    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("Julie", "Jones"));
    current_employee_list.Push_back(employee_t("John", "Smith"));

    std::observer_ptr<employee_t> p_person_who_convinces_boss_to_rehire_him(&(current_employee_list.back()));
    replace_last_employee_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

Il n'est peut-être jamais venu à l'esprit de l'auteur de la fonction replace_last_employee_with() que la référence à la nouvelle embauche pourrait également être une référence à l'employé existant à remplacer, auquel cas la fonction peut provoquer par inadvertance la cible de son Le paramètre std::observer_ptr<employee_t> Doit être désalloué avant qu'il ne soit fini de l'utiliser.

C'est un exemple artificiel, mais ce genre de chose peut facilement se produire dans des situations plus complexes. Bien sûr, l'utilisation de pointeurs bruts est parfaitement sûre dans la grande majorité des cas. Le problème est qu'il existe une minorité de cas où il est facile de supposer que c'est sûr alors qu'il ne l'est vraiment pas.

Si le remplacement du paramètre std::observer_ptr<employee_t> Par un std::shared_ptr Ou std::weak_ptr Est pour une raison quelconque inacceptable, il existe maintenant une autre option sûre - et c'est la partie prise sans vergogne de la réponse - " pointeurs enregistrés ". Les "pointeurs enregistrés" sont des pointeurs intelligents qui se comportent exactement 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é. Ils sont généralement plus rapides que std :: shared_ptrs, mais si vos exigences de performances sont vraiment strictes, les pointeurs enregistrés peuvent être "désactivés" (remplacés automatiquement par leur équivalent de pointeur brut) avec une directive de compilation, permettant ils doivent être utilisés (et engendrer des frais généraux) dans les modes débogage/test/bêta uniquement.

Donc, s'il doit y avoir un pointeur "observateur" basé sur des pointeurs bruts, alors il devrait sans doute y en avoir un basé sur des pointeurs enregistrés et peut-être comme l'OP l'a suggéré, un basé sur std :: shared_ptr également.

2
Noah

Mis à part le cas d'utilisation de la documentation, il existe des problèmes réels qui peuvent se produire lors du contournement de pointeurs bruts sans la décoration de l'observateur. Un autre code peut incorrectement assumer la responsabilité à vie des pointeurs bruts et transmettre le pointeur à la propriété en prenant std::unique_ptr, std::shared_ptr, ou tout simplement disposer de l'objet via delete.

Cela est particulièrement vrai pour le code hérité qui peut être mis à niveau lorsque les règles de propriété ne sont pas entièrement établies. Le observer_ptr permet d'appliquer la règle selon laquelle la durée de vie de l'objet ne peut pas être transférée.

Prenons l'exemple suivant:

#include <iostream>
#include <memory>

struct MyObject
{
    int value{ 42 };
};

template<typename T>
void handlerForMyObj(T ptr) noexcept
{
    if (42 != ptr->value) {
        // This object has gone rogue. Dispose of it!
        std::cout << "The value must be 42 but it's actually " << ptr->value << "!\n";
        delete ptr;
        return;
    }
    std::cout << "The value is  " << ptr->value << ".\n";
}

void func1()
{
    MyObject myObj;
    MyObject *myObjPtr = &myObj; 

    myObj.value = 24;

    // What?! Likely run-time crash. BOO!
    handlerForMyObj(myObjPtr);
}

void func2()
{
    MyObject myObj;
    std::observer_ptr<MyObject> myObjObserver{ &myObj };

    myObj.value = 24;

    // Nice! Compiler enforced error because ownership transfer is denied!
    handlerForMyObj(myObjObserver);
}

int main(int argn, char *argv[])
{
    func1();
    func2();
}

Dans le cas du pointeur brut, la suppression incorrecte de l'objet peut uniquement être découverte au moment de l'exécution. Mais dans le observer_ptr cas, l'opérateur delete ne peut pas être appliqué à l'observateur.

0
Robin R