Search code examples
c++shared-ptrsoftware-designweak-ptr

C++ 11 Smart Pointer Ownership and Casting


I have a base entity class and derived classes like cows and chickens...

using namespace std;
class Entity
{
    list<shared_ptr<Relationship>> relationships;
    void createRelationship(weak_ptr<Entity> other,.... other stuff)
    //...
    virtual ~Entity()
}
class Cow: public Entity
{
    //...
}
class Chicken : public Entity
....etc...

I am trying to learn to manage memory properly with std smart pointers. The way I am doing things now, the only place my derived classes ever live is in vectors of shared pointers like...

vector<shared_ptr<Cow>>
vector<shared_ptr<Chicken>>
etc.

My Entity class is in charge of managing relationships between any two entities, whether they have the same type or not. To do this, it keeps a list of Relationship objects that look like...

class Relationship
{
  weak_ptr<Entity> from;
  weak_ptr<Entity> to;
etc....
}

I use weak pointers because the cows or chickens may die, and in that case their relationships with other entities should become invalid.

So here is my problem. I store everything as shared pointers to derived classes, but all of my code in my Entity class uses weak pointers to the base class. I will somewhat often need to convert weak Entity pointers to shared Cow pointers or shared Cow pointers down to weak Entity pointers.

Somehow, my code allows me to pass shared_ptr objects in the parameter in createRelationship(...) in the Entity class above. I don't really know why that compiles, and I would like to know if it's efficient to do that. Should I instead manually turn it into a weak pointer and then cast using static_pointer_cast? (I ask this because I've read that passing shared pointers as parameters is slow, and I'm worried that's going on).

In the other direction, sometimes I know that certain relationships are between two entities of the same type. To illustrate my point, a baby cow that needs to inherit its genetics from its parents: It searches through its relationships to find parent-child relationships, and then gets access to weak entity pointers to its parents. In order to access their genetic member variables, it needs to cast these weak pointers to Entities to shared pointers to Cows. I've been using weak_ptr.lock() followed by dynamic_pointer_cast to accomplish this.

Is this the efficient way of performing these two (inversely related) casts? Any general remarks or reference material is appreciated, because I'm trying to get a hold on using these pointers efficiently.


Solution

  • It sounds like you have three main concerns:

    1. Is storing weak_ptr efficient when you have to frequently convert to shared_ptr to use its value?
    2. Why can you construct a weak_ptr<T> from a shared_ptr<T>?
    3. Are you better off using static_ptr_cast or dynamic_pointer_cast

    Question 2 is the easiest; as Vaughn mentions, weak_ptr has a constructor from shared_ptr.

    Question 1 and 3 are much more murky. To address this, let's visit why you've heard that passing shared_ptr is slow. When you pass around a shared_ptr by value, it must copy the shared_ptr, and copying it involves an atomic increment of the underlying reference count. There are lots of benefits and downsides to such atomic increments, but the short version is that if you don't need to track ownership, it is an unnecessary overhead. (Worrying about this in most cases might well be premature optimization, but C++ the language wants to ensure that it's possible for you to worry about this sort of thing when it turns out to be necessary.)

    What about copying a weak_ptr -- is that any faster than copying a shared_ptr? I haven't run any benchmarks, but I would guess not. There are actually two reference counts, one each for owning references (shared_ptr copies) and for non-owning (weak_ptr copies). Each of these will have the same atomic update requirements, so will not be significantly faster. I guess in theory that the destructor of the weak_ptr won't have to check the resulting reference count and delete the object, so that is one less branch if all you're doing is copying. But that's an unlikely usage; chances are you'll be converting back to a shared_ptr via lock().

    And that brings us back to the core of question 1. How much overhead is there to getting the shared_ptr from an observing weak_ptr? Approximately as much as the atomic reference count of copying a shared_ptr, plus the branch you'll require on the consuming code to ensure that it succeeded, or handle when it failed. So rather than considering efficiency here, it's better to consider ownership and object lifetimes. Will you ever be in a situation where the lock() will return a null shared_ptr? If not, chances are you can get away with an observing raw pointer. If the underlying shared_ptr object may disappear before the weak_ptr does, you'll need its expiration check. If this is a bottleneck, see if you can't find a way to guarantee the lifetimes.

    Finally back to question 3. I'm answering here without any real knowledge of these types; instead I'm basing it off of how a shared_ptr works. Each of static_pointer_cast, dynamic_pointer_cast, and const_pointer_cast return shared_ptr instances that point to the same underlying object. Thus they have performed the atomic increment of its reference count. So their overhead is roughly that of a static or dynamic or const cast plus that of a shared_ptr copy constructor. The shared_ptr part is unlikely to be significant to your overall program, and of the casting part, only the dynamic_cast of a dynamic_pointer_cast could have a measurable expense (static and const casts are almost entirely compile-time operations).

    So once again we are back to ownership and object lifetimes. If you have a clear ownership and that works to give you the lifetime you want, and if you are also writing code that is at the bottleneck of your performance, you will be happier with unique_ptr and observing raw pointers (and that's fine; the "rule" is no owning raw pointers). But if it's non-bottleneck, or if the object lifetime is not so easy to guarantee, shared_ptr and weak_ptr are definitely conveniences with as small a cost as possible to guarantee their semantics.