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?
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.
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
.
Oui.
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*
:
observer_ptr
sera toujours initialisé à nullptr
; un pointeur normal peut ou non être initialisé, selon le contexte.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.observer_ptr
, car ce sont des opérations itérateur.observer_ptr
s 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
).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
)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;
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.
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.