Utilisation de pointeurs intelligents pour les membres de la classe

159

J'ai du mal à comprendre l'utilisation des pointeurs intelligents en tant que membres de classe dans C ++ 11. J'ai beaucoup lu sur les pointeurs intelligents et je pense comprendre comment unique_ptret shared_ptr/ weak_ptrfonctionne en général. Ce que je ne comprends pas, c'est l'usage réel. Il semble que tout le monde recommande de l'utiliser unique_ptrpresque tout le temps. Mais comment pourrais-je implémenter quelque chose comme ceci:

class Device {
};

class Settings {
    Device *device;
public:
    Settings(Device *device) {
        this->device = device;
    }

    Device *getDevice() {
        return device;
    }
};    

int main() {
    Device *device = new Device();
    Settings settings(device);
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

Disons que je voudrais remplacer les pointeurs par des pointeurs intelligents. Un unique_ptrne fonctionnerait pas à cause de getDevice(), non? C'est donc le moment où j'utilise shared_ptret weak_ptr? Pas moyen d'utiliser unique_ptr? Il me semble que dans la plupart des cas, shared_ptrcela a plus de sens, sauf si j'utilise un pointeur dans une très petite portée?

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> device) {
        this->device = device;
    }

    std::weak_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::weak_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Est-ce la voie à suivre? Merci beaucoup!

Michaelk
la source
4
Cela aide à être vraiment clair sur la durée de vie, la propriété et les éventuelles nullités. Par exemple, après avoir passé deviceau constructeur de settings, voulez-vous pouvoir toujours y faire référence dans la portée appelante, ou uniquement via settings? Si ce dernier, unique_ptrest utile. Vous avez également un scénario dans lequel la valeur de retour de getDevice()est null. Sinon, renvoyez simplement une référence.
Keith
2
Oui, a shared_ptrest correct dans 8/10 cas. Les 2/10 restants sont répartis entre unique_ptret weak_ptr. En outre, weak_ptrest généralement utilisé pour casser des références circulaires; Je ne suis pas sûr que votre utilisation soit considérée comme correcte.
Collin Dauphinee
2
Tout d'abord, quelle propriété souhaitez-vous pour le devicemembre de données? Vous devez d'abord en décider.
juanchopanza
1
Ok, je comprends qu'en tant qu'appelant, je pourrais utiliser un à la unique_ptrplace et abandonner la propriété lors de l'appel du constructeur, si je sais que je n'en aurai plus besoin pour le moment. Mais en tant que concepteur de la Settingsclasse, je ne sais pas si l'appelant souhaite également conserver une référence. Peut-être que l'appareil sera utilisé dans de nombreux endroits. Ok, c'est peut-être exactement votre point. Dans ce cas, je ne serais pas le seul propriétaire et c'est à ce moment-là que j'utiliserais shared_ptr, je suppose. Et: les points intelligents remplacent donc les pointeurs, mais pas les références, non?
michaelk
this-> appareil = appareil; Utilisez également des listes d'initialisation.
Nils

Réponses:

202

Un unique_ptrne fonctionnerait pas à cause de getDevice(), non?

Non pas forcément. L'important ici est de déterminer la politique de propriété appropriée pour votre Deviceobjet, c'est-à-dire qui sera le propriétaire de l'objet pointé par votre pointeur (intelligent).

Est-ce que ce sera l'instance de l' Settingsobjet seul ? L' Deviceobjet devra-t-il être détruit automatiquement lorsque l' Settingsobjet sera détruit, ou devrait-il survivre à cet objet?

Dans le premier cas, std::unique_ptrc'est ce dont vous avez besoin, car il fait Settingsle seul (unique) propriétaire de l'objet pointé, et le seul objet qui est responsable de sa destruction.

Sous cette hypothèse, getDevice()devrait retourner un simple pointeur d' observation (les pointeurs d'observation sont des pointeurs qui ne maintiennent pas l'objet pointé en vie). Le type le plus simple de pointeur d'observation est un pointeur brut:

#include <memory>

class Device {
};

class Settings {
    std::unique_ptr<Device> device;
public:
    Settings(std::unique_ptr<Device> d) {
        device = std::move(d);
    }

    Device* getDevice() {
        return device.get();
    }
};

int main() {
    std::unique_ptr<Device> device(new Device());
    Settings settings(std::move(device));
    // ...
    Device *myDevice = settings.getDevice();
    // do something with myDevice...
}

[ NOTE 1: Vous vous demandez peut-être pourquoi j'utilise des pointeurs bruts ici, alors que tout le monde n'arrête pas de dire que les pointeurs bruts sont mauvais, dangereux et dangereux. En fait, c'est un avertissement précieux, mais il est important de le mettre dans le bon contexte: les pointeurs bruts sont mauvais lorsqu'ils sont utilisés pour effectuer une gestion manuelle de la mémoire , c'est-à-dire allouer et désallouer des objets via newet delete. Lorsqu'ils sont utilisés uniquement comme un moyen d'atteindre une sémantique de référence et de faire circuler des pointeurs d'observation non propriétaires, il n'y a rien d'intrinsèquement dangereux dans les pointeurs bruts, sauf peut-être le fait qu'il faut veiller à ne pas déréférencer un pointeur suspendu. - FIN NOTE 1 ]

[ NOTE 2: Comme il est apparu dans les commentaires, dans ce cas particulier où la propriété est unique et où l'objet possédé est toujours garanti d'être présent (c'est-à-dire que le membre de données interne devicene le sera jamais nullptr), la fonction getDevice()pourrait (et devrait peut-être) renvoie une référence plutôt qu'un pointeur. Bien que cela soit vrai, j'ai décidé de renvoyer un pointeur brut ici parce que je voulais dire que c'était une réponse courte que l'on pourrait généraliser au cas où cela devicepourrait être nullptr, et pour montrer que les pointeurs bruts sont OK tant que l'on ne les utilise pas pour gestion manuelle de la mémoire. - FIN NOTE 2 ]


La situation est bien sûr radicalement différente si votre Settingsobjet ne doit pas avoir la propriété exclusive de l'appareil. Cela pourrait être le cas, par exemple, si la destruction de l' Settingsobjet ne devait pas également impliquer la destruction de l' Deviceobjet pointé .

C'est quelque chose que seul vous, en tant que concepteur de votre programme, pouvez dire; d'après l'exemple que vous donnez, il m'est difficile de dire si c'est le cas ou non.

Pour vous aider à le comprendre, vous pouvez vous demander s'il existe d'autres objets en dehors de ceux Settingsqui ont le droit de maintenir l' Deviceobjet en vie tant qu'ils tiennent un pointeur vers lui, au lieu d'être simplement des observateurs passifs. Si tel est effectivement le cas, vous avez besoin d'une politique de propriété partagée , qui std::shared_ptroffre:

#include <memory>

class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(std::shared_ptr<Device> const& d) {
        device = d;
    }

    std::shared_ptr<Device> getDevice() {
        return device;
    }
};

int main() {
    std::shared_ptr<Device> device = std::make_shared<Device>();
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice = settings.getDevice();
    // do something with myDevice...
}

Remarquez qu'il weak_ptrs'agit d'un pointeur d' observation , pas d'un pointeur propriétaire - en d'autres termes, il ne maintient pas l'objet pointé en vie si tous les autres pointeurs propriétaires vers l'objet pointé sont hors de portée.

L'avantage par weak_ptrrapport à un pointeur brut ordinaire est que vous pouvez dire en toute sécurité s'il weak_ptrest suspendu ou non (c'est-à-dire s'il pointe vers un objet valide ou si l'objet pointé à l'origine a été détruit). Cela peut être fait en appelant la expired()fonction membre sur l' weak_ptrobjet.

Andy Prowl
la source
4
@LKK: Oui, c'est exact. A weak_ptrest toujours une alternative aux pointeurs d'observation bruts. Il est plus sûr dans un sens, car vous pouvez vérifier s'il est suspendu avant de le déréférencer, mais il comporte également des frais généraux. Si vous pouvez facilement garantir que vous n'allez pas déréférencer un pointeur suspendu, alors vous devriez être d'accord avec l'observation des pointeurs bruts
Andy Prowl
6
Dans le premier cas, il serait probablement même préférable de laisser getDevice()renvoyer une référence, n'est-ce pas? Ainsi, l'appelant n'aurait pas à vérifier nullptr.
vobject
5
@chico: Je ne sais pas ce que tu veux dire. auto myDevice = settings.getDevice()va créer une nouvelle instance du type Deviceappelé myDeviceet la copier-construire à partir de celle référencée par la référence qui getDevice()retourne. Si vous voulez myDeviceêtre une référence, vous devez le faire auto& myDevice = settings.getDevice(). Donc, à moins que je ne manque quelque chose, nous sommes de retour dans la même situation que nous avions sans utiliser auto.
Andy Prowl
2
@Purrformance: Parce que vous ne voulez pas céder la propriété de l'objet - remettre un modifiable unique_ptrà un client ouvre la possibilité que le client en quitte, acquérant ainsi la propriété et vous laissant avec un pointeur nul (unique).
Andy Prowl
7
@Purrformance: Bien que cela empêcherait un client de déménager (à moins que le client ne soit un scientifique fou passionné par les const_casts), personnellement, je ne le ferais pas. Il expose un détail de mise en œuvre, c'est-à-dire le fait que la propriété est unique et réalisée à travers un unique_ptr. Je vois les choses de cette façon: si vous voulez / devez transmettre / retourner la propriété, passez / renvoyez un pointeur intelligent ( unique_ptrou shared_ptr, selon le type de propriété). Si vous ne voulez / devez pas transmettre / retourner la propriété, utilisez un constpointeur ou une référence (correctement qualifié), principalement selon que l'argument peut être nul ou non.
Andy Prowl
0
class Device {
};

class Settings {
    std::shared_ptr<Device> device;
public:
    Settings(const std::shared_ptr<Device>& device) : device(device) {

    }

    const std::shared_ptr<Device>& getDevice() {
        return device;
    }
};

int main()
{
    std::shared_ptr<Device> device(new Device());
    Settings settings(device);
    // ...
    std::shared_ptr<Device> myDevice(settings.getDevice());
    // do something with myDevice...
    return 0;
}

week_ptrest utilisé uniquement pour les boucles de référence. Le graphe de dépendances doit être un graphe dirigé acyclique. Dans les pointeurs partagés, il existe 2 décomptes de références: 1 pour shared_ptrs et 1 pour tous les pointeurs ( shared_ptret weak_ptr). Lorsque tous les shared_ptrs sont supprimés, le pointeur est supprimé. Lorsque le pointeur est nécessaire à partir de weak_ptr, lockdoit être utilisé pour obtenir le pointeur, s'il existe.

Naszta
la source
Donc, si je comprends bien votre réponse, les pointeurs intelligents remplacent les pointeurs bruts, mais pas nécessairement les références?
michaelk
Y a-t-il en fait deux nombres de références dans un shared_ptr? Pouvez-vous expliquer pourquoi? Pour autant que je sache, weak_ptrne doit pas être compté car il crée simplement un nouveau shared_ptrlors de l'opération sur l'objet (si l'objet sous-jacent existe toujours).
Björn Pollex
@ BjörnPollex: J'ai créé un petit exemple pour vous: lien . Je n'ai pas tout implémenté juste les constructeurs de copie et lock. La version boost est également thread-safe sur le comptage de références ( deleten'est appelée qu'une seule fois).
Naszta
@Naszta: Votre exemple montre qu'il est possible de l'implémenter en utilisant deux nombres de références, mais votre réponse suggère que cela est nécessaire , ce que je ne crois pas. Pourriez-vous clarifier cela dans votre réponse?
Björn Pollex
1
@ BjörnPollex, pour weak_ptr::lock()savoir si l'objet a expiré, il doit inspecter le "bloc de contrôle" qui contient le premier décompte de références et le pointeur vers l'objet, de sorte que le bloc de contrôle ne doit pas être détruit tant qu'il y a des weak_ptrobjets encore en cours d'utilisation, le nombre d' weak_ptrobjets doit donc être suivi, ce que fait le deuxième décompte de références. L'objet est détruit lorsque le premier nombre de références tombe à zéro, le bloc de contrôle est détruit lorsque le deuxième compte de références tombe à zéro.
Jonathan Wakely