Using smart pointers for class members

来源:互联网 发布:string.h 51单片机 编辑:程序博客网 时间:2024/05/16 15:38
112 down vote favorite
78

I'm having trouble understanding the usage of smart pointers as class members in C++11. I have read a lot about smart pointers and I think I do understand howunique_ptr and shared_ptr/weak_ptr work in general. What I don't understand is the real usage. It seems like everybody recommends usingunique_ptr as the way to go almost all the time. But how would I implement something like this:

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...}

Let's say I would like to replace the pointers with smart pointers. A unique_ptr would not work because ofgetDevice(), right? So that's the time when I use shared_ptr andweak_ptr? No way of using unique_ptr? Seems to me like for most casesshared_ptr makes more sense unless I'm using a pointer in a really small scope?

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...}

Is that the way to go? Thanks very much!

shareimprove this question
 
4 
It helps to be really clear as to lifetime, ownership and possible nulls. For example, having passeddevice to the constructor of settings, do you want to still be able to refer to it in the calling scope, or only viasettings? If the latter, unique_ptr is useful. Also, do you have a scenario where the return value ofgetDevice() is null. If not, just return a reference. – KeithMar 26 '13 at 22:53
2 
Yes, ashared_ptr is correct in 8/10 cases. The other 2/10 are split betweenunique_ptr and weak_ptr. Also, weak_ptr is generally used to break circular references; I'm not sure that your usage would be considered correct. – Collin Dauphinee Mar 26 '13 at 22:53
2 
First of all, what ownership do you want for thedevice data member? You first have to decide that. – juanchopanzaMar 26 '13 at 22:56
1 
Ok, I understand that as the caller I could use aunique_ptr instead and give the ownership up when calling the constructor, if I know I won't need it anymore for now. But as the designer of theSettings class I don't know if the caller wants to keep a reference as well. Maybe the device will be used in many places. Ok, maybe that's exactly your point. In that case, I would not be the sole owner and that's when I would use shared_ptr, I guess. And: so smart points do replace pointers, but not references, right? – michaelkMar 26 '13 at 23:02
 
this->device = device; Also use initialization lists. – NilsMar 27 '13 at 11:02

2 Answers

activeoldest votes
up vote141 down vote accepted

A unique_ptr would not work because of getDevice(), right?

No, not necessarily. What is important here is to determine the appropriate ownership policy for your Device object, i.e. who is going to be the owner of the object pointed to by your (smart) pointer.

Is it going to be the instance of the Settings object alone? Will theDevice object have to be destroyed automatically when the Settings object gets destroyed, or should it outlive that object?

In the first case, std::unique_ptr is what you need, since it makesSettings the only (unique) owner of the pointed object, and the only object which is responsible for its destruction.

Under this assumption, getDevice() should return a simple observing pointer (observing pointers are pointers which do not keep the pointed object alive). The simplest kind of observing pointer is a raw pointer:

#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: You may be wondering why I am using raw pointers here, when everybody keeps telling that raw pointers are bad, unsafe, and dangerous. Actually, that is a precious warning, but it is important to put it in the correct context: raw pointers are bad when used for performing manual memory management, i.e. allocating and deallocating objects throughnew and delete. When used purely as a means to achieve reference semantics and pass around non-owning, observing pointers, there is nothing intrinsically dangerous in raw pointers, except maybe for the fact that one should take care not to dereference a dangling pointer. - END NOTE 1]

[NOTE 2: As it emerged in the comments, in this particular case where the ownership is uniqueand the owned object is always guaranteed to be present (i.e. the internal data memberdevice is never going to be nullptr), function getDevice() could (and maybe should) return a reference rather than a pointer. While this is true, I decided to return a raw pointer here because I meant this to be a short answer that one could generalize to the case where device could be nullptr, and to show that raw pointers are OK as long as one does not use them for manual memory management. -END NOTE 2]


The situation is radically different, of course, if your Settings object shouldnot have the exclusive ownership of the device. This could be the case, for instance, if the destruction of theSettings object should not imply the destruction of the pointed Device object as well.

This is something that only you as a designer of your program can tell; from the example you provide, it is hard for me to tell whether this is the case or not.

To help you figure it out, you may ask yourself whether there are any other objects apart fromSettings that are entitled to keep the Device object alive as long as they hold a pointer to it, instead of being just passive observers. If that is indeed the case, then you need ashared ownership policy, which is what std::shared_ptr offers:

#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...}

Notice, that weak_ptr is an observing pointer, not an owning pointer - in other words, it does not keep the pointed object alive if all other owning pointers to the pointed object go out of scope.

The advantage of weak_ptr over a regular raw pointer is that you can safely tell whetherweak_ptr is dangling or not (i.e. whether it is pointing to a valid object, or if the object originally pointed to has been destroyed). This can be done by calling theexpired() member function on the weak_ptr object.

shareimprove this answer
 
3 
@LKK: Yes, correct. Aweak_ptr is always an alternative to raw observing pointers. It is safer in a sense, because you could check if it is dangling before dereferencing it, but it also comes with some overhead. If you can easily guarantee that you are not going to dereference a dangling pointer, then you should be fine with observing raw pointers – Andy ProwlMar 26 '13 at 23:24
4 
In the first case it would probably even be better to letgetDevice() return a reference, whouldn't it? So the caller would not have to check fornullptr. – vobjectMar 27 '13 at 10:23
5 
@chico: Not sure what you mean.auto myDevice = settings.getDevice() will create a new instance of typeDevice called myDevice and copy-construct it from the one referenced by the reference thatgetDevice() returns. If you want myDevice to be a reference, you need to doauto& myDevice = settings.getDevice(). So unless I am missing something, we're back in the same situation we had without usingauto. – Andy ProwlMar 27 '13 at 16:28
2 
@Purrformance: Because you don't want to give away the ownership of the object - handing a modifiableunique_ptr to a client opens the possibility that the client will move from it, thus acquiring ownership and leaving you with a null (unique) pointer. – Andy Prowl Jan 29 '14 at 21:52
7 
@Purrformance: While that would prevent a client from moving (unless the client is a mad scientist keen onconst_casts), I personally wouldn't do it. It exposes an implementation detail, i.e. the fact that ownership is unique and realized through aunique_ptr. I see things this way: if you want/need to pass/return ownership, pass/return a smart pointer (unique_ptr orshared_ptr, depending on the kind of ownership). If you don't want/need to pass/return ownership, use a (properlyconst-qualified) pointer or reference, mostly depending on whether the argument can be null or not. – Andy ProwlJan 29 '14 at 22:49
up vote0 down vote
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_ptr is used only for reference loops. The dependency graph must be acyclicdirected graph. In shared pointers there are 2 reference counts: 1 forshared_ptrs, and 1 for all pointers (shared_ptr andweak_ptr). When all shared_ptrs are removed, the pointer is deleted. When pointer is needed fromweak_ptr, lock should be used to get the pointer, if it exists.

shareimprove this answer
 
 
So if I understand your answer correctly, smart pointers do replace raw pointers, but not necessarily references? – michaelkMar 26 '13 at 23:10
 
LKK: Yes. True. – NasztaMar 26 '13 at 23:14
 
Are there actuallytwo reference counts in a shared_ptr? Can you please explain why? As far as I understand,weak_ptr doesn't have to be counted because it simply creates a new shared_ptr when operating on the object (if the underlying object still exists). – Björn PollexMar 27 '13 at 8:35
 
@BjörnPollex: I created a short example for you:link. I haven't implemented everything just the copy constructors and lock.boost version is also thread safe on reference counting (delete is called only once). – NasztaMar 27 '13 at 20:45
 
@Naszta: Your example shows that it ispossible to implement this using two reference counts, but your answer suggests that this isrequired, which I don't believe it is. Could you please clarify this in your answer? – Björn PollexMar 28 '13 at 8:01