Skip to main content
GameDev.net gamedev.net
🔒 Locked

c++ shared_ptr usage

Started by Misantes Apr 26, 2015 at 9:32 PM 7 replies 2.8k views
Original Post
Misantes
Misantes

A quick question regarding the usage of a shared pointer in c++, and my apologies if this question is rather dimwitted:

Would an approach like this be considered poor design, or is this what shared_ptr's are for:

Let's say I have a class A and B


class A
{
    public:
        int firstInteger = 0;
};

class B
{
    public:
        void functionThatUsesA(A &refToInstanceA); //have to pass in reference to A every iteration
}; 

Rather than pass A into the function of B every iteration, could I just create a shared_ptr set to a shared_ptr of instance A?


class A
{
    public:
        int firstInteger = 0;
};

class B
{
    public:
        void setPointerToA(std::shared_ptr &refToA);
        void functionThatUsesA(); 
    private:
        std::shared_ptr pointerToA;
};

void B::setPointerToA(std::shared_ptr &refToA)
{
    pointerToA = refToA;
}
void B::functionThatUsesA() //no longer needs the reference to class A passed in
{
    pointerToA.firstInteger +=1;
}

This does work, and the benefit (as I am seeing it), is that I don't have to pass in a reference to the first class every iteration, as the second class has a shared_ptr to work with.

However, I worry there are some design pitfalls I'm overlooking here, or performance issues I'm not seeing, or simply something I'm unaware of (I would guess it's actually easier on the cpu as it's not copying anything for the parameter, but I haven't profiled it, and I could be mistaken). I can imagine this getting rather convoluted, if not used sparingly (I imagine if the shared_ptr count got rather high, it could be difficult to debug where exactly you're doing something to the object it references).

Anyhow, my question is whether this is a terrible idea, for reasons I'm overlooking, or if this would be considered a rather typical use of shared_ptrs and is completely recommended? I find plenty of information out there of how shared_ptrs work but little in the way of practical use cases for them. I'd welcome any advice, whether general or specific here. Thanks in advance :)

Beginner here <- please take any opinions with grain of salt
Mussi
Mussi

Passing as argument or storing as member are both valid designs. The examples you've given are quite distinct though. Calling the method with different instances of A is easy in the first example, less so in the second. If only a single instance of A should be coupled to B, then passing the pointer to the constructor would more clearly describe that intent.

There's also a difference in lifetime management, in your first example class B has no control over the lifetime of A, the second example does.

Depending on your requirements you can choose one over the other.

3pic_F4il_FTW
3pic_F4il_FTW
Depends on whether A can be considered a shared resource and if its lifetime should be bound to B's lifetime, as well as the necessity of being able to set the pointer from outside I would say. But I cant really tell from that context.
Misantes
Misantes

Yeah, my genuine apologies for the overly simplistic example. I glossed over the original creation of the shared_ptr that's passed to B's function. But, I didn't want to over complicate the example. Assuming that the originally created shared_ptr of A is still in scope, B could go out of scope or be deleted without affecting the lifetime of the original shared_ptr as long as there is still one in scope, correct (as long as the count is still > 0)? I'm probably still being too vague, but I'm fairly certain I understand the management of the shared_ptr, so don't feel obligated to clarify this point. I was more concerned about performance, or design decisions regarding keeping a separate copy for the purpose of avoiding passing in the same reference to another class every iteration.

I definitely see your point regarding the functionality Mussi. I think for my intended purposes, B's function would always refer to the same Object (or, if not, the pointer would be updated). But, I definitely see how the function becomes limited as it can only ever use the same object that's pointed to. That's definitely something to keep in mind.

Beginner here <- please take any opinions with grain of salt
3pic_F4il_FTW
3pic_F4il_FTW

Assuming that the originally created shared_ptr is still in scope, B could go out of scope or be deleted without affecting the lifetime of the original shared_ptr as long as there is still one in scope, correct (as long as the count is still > 0)?

That would be true for a raw pointer (as long as you dont delete it in Bs destructor) or a member reference as well.
The shared pointer ensures that the above is true AND that A will live at least as long as B is an owner of it. If the resource lifetime should be handled by a different part of your program B shouldnt be considered an Owner (but that depend on your use case)

If you CAN guarantee that A wont go out of scope for B's entire lifetime and you will always be using the same instance of A a member reference to A might be fine as well. Also a raw pointer would be fine (null checking!) or a weak pointer to an already existing shared pointer.

Also the passing via parameters can have its advantages. But again that depends on what A and B are actually supposed to do.

Can you maybe tell us a bit more about your use case?
blutzeit
blutzeit

I think in order to understand shared_ptr, you should first understand unique_ptr. To fully understand the unique_ptr you need to also understand move semantics (and e.g. how/why unique_ptr is very useful for factory methods). Then you can move on to cases where you need to access the same unique_ptr from different places, and need to convert the unique_ptr into a shared_ptr. And finally there's weak_ptr, that's useful in cases where you have circular references or cached objects. The point is, in order to understand unique_ptr you need to understand all three.

openwar  - the real-time tactical war-game platform
samoth
samoth

What you are doing can be very advantageous if you plan to move the loop calling functionThatUsesA to another thread later. The shared pointer has shared ownership of the object, so it will keep the object alive for as long as it lives. Which means that if thread 1 fires off thread 2 to do that work and then decides that it no longer needs the resource, it needs not pay attention to synchronize with whoever might need it -- there will be no funny stuff happening with thread 2 accessing an object that doesn't exist any more.

On the other hand, if you are writing strictly serial code (as I am inclined to believe from what I've read) then the shared pointer is unnecessary. It's technically correct, but you don't need it. You can guarantee that the pointer will always be valid anyway, so a raw pointer will do, too. From a purely semantic point of view, however, one shouldn't use a shared pointer, if sharing ownership isn't what you mean to express.

As to storing as member versus passing as argument, passing a pointer as function argument costs very little if anything. With any luck, the call is inlined anyway, or the parameter is "passed" by re-coloring (is that the right word?) a register so the net overhead is zero. Reading a member from memory may very well be slower than passing the parameter (though most likely it will be in L1 if the compiler isn't smart enough to optimize that out anyway).

Using a shared pointer would be rather unwise if you were passing it with every iteration (which you're not doing, luckily). You need to be aware that passing a (non-reference) shared pointer will increment the reference count, and the function returning will decrement it, both being atomic operations.

These are not "free" operations, and this can be something that may affect performance, even more so if you have used make_shared. It is usually recommended to use make_shared since that allocates the reference count and the object in one block, which is an optimization (both in memory and runtime). However, on the other hand, an atomic operation is not just somewhat slower, but also invalidates the accessed cache line, which basically means if your object lives together with the ref count, you have a guaranteed cache miss the first time you access the object after passing the shared pointer. Which doesn't matter if you do it a hundred times per frame, but it greatly matters if you do it ten thousand or a hundred thousand times.

Pink Horror
Pink Horror




However, I worry there are some design pitfalls I'm overlooking here, or performance issues I'm not seeing, or simply something I'm unaware of (I would guess it's actually easier on the cpu as it's not copying anything for the parameter, but I haven't profiled it, and I could be mistaken). I can imagine this getting rather convoluted, if not used sparingly (I imagine if the shared_ptr count got rather high, it could be difficult to debug where exactly you're doing something to the object it references).



Anyhow, my question is whether this is a terrible idea, for reasons I'm overlooking, or if this would be considered a rather typical use of shared_ptrs and is completely recommended? I find plenty of information out there of how shared_ptrs work but little in the way of practical use cases for them. I'd welcome any advice, whether general or specific here. Thanks in advance

If you're doing this for performance, it sounds like a bad idea.

With a reference or raw pointer passed as an argument, the only thing that might be copied is a pointer that is currently in a register or on the stack, and it will be copied into a register or onto the stack. Also, by adding another parameter, you might cause other reads or writes involving the stack, saving and restoring registers or forcing other arguments onto the stack. So, it's not guaranteed to be free, but register copies are pretty close to free compared to memory access, and stack access is likely to use the cache. There's also a good opportunity for inlining such copies away.

With a shared_ptr in each object that relies on some resource, when that pointer is used, it has to be copied from memory into a register. Depending on how often you use the pointer and what code your compiler has available when it is optimizing, that load might happen just once or multiple times in a function - it might not be able to assume the pointer stays the same. Also, if you have many different objects with the same pointer - and you suggest this will be the case when you mention ref counts - every different copy of the pointer has to be loaded from memory before it is used. I sincerely doubt the compiler will be "smart enough" to treat them like they all point to the same thing. If you imagine a loop calling the same function on a bunch of objects using this pointer, that loop goes from loading a pointer from the same location on the stack each frame (or using registers) to skipping through memory. Now, you're probably using other chunks of memory from these objects, so it's not that bad, but it's still probably worse than the problem you're trying to solve.

And, if you use this pattern often for many different classes, you will also likely run into the problem of circular references. You'll have to switch to weak_ptr or add unitialize functions to unset some shared_ptrs to allow things to be deleted.

Misantes
Misantes

Ok, some of these touched on my pressing concern of performance. In my actual implementation of things, I'm currently just passing in a reference to unique_ptrs. If there's really little performance difference, then simply continuing to do things this way seems fine and likely less problematic.

Beginner here <- please take any opinions with grain of salt
SmkViper
SmkViper

Ok, some of these touched on my pressing concern of performance. In my actual implementation of things, I'm currently just passing in a reference to unique_ptrs. If there's really little performance difference, then simply continuing to do things this way seems fine and likely less problematic.


I would say generally you shouldn't be passing smart pointers of any kind around unless you are transferring or modifying ownership.

If a function does not need to own an object then just pass it by pointer (if null is valid) or reference (if null is not valid). This is because your parameters are part of your interface - if you are taking a smart pointer in your parameter then I don't know if you intend to take ownership, or shared ownership of the object. If you are just taking a pointer or reference then I know you won't modify ownership and expect the object to last the lifetime of the function. You can also generally assume the function isn't going to go store the pointer somewhere else for safe keeping (because doing so would potentially result in a bad pointer when the object is deleted).

Also, a smart pointer can be slower to access then a regular pointer or reference. Granted, compilers can potentially optimize this out, but that's much harder if you're using some sort of shared/weak pointer system, especially if it is designed to be thread-safe.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.