Smart Pointers: Who Owns the Object

Smart pointers: who owns the object?

For me, these 3 kinds cover most of my needs:

shared_ptr - reference-counted, deallocation when the counter reaches zero

weak_ptr - same as above, but it's a 'slave' for a shared_ptr, can't deallocate

auto_ptr - when the creation and deallocation happen inside the same function, or when the object has to be considered one-owner-only ever. When you assign one pointer to another, the second 'steals' the object from the first.

I have my own implementation for these, but they are also available in Boost.

I still pass objects by reference (const whenever possible), in this case the called method must assume the object is alive only during the time of call.

There's another kind of pointer that I use that I call hub_ptr. It's when you have an object that must be accessible from objects nested in it (usually as a virtual base class). This could be solved by passing a weak_ptr to them, but it doesn't have a shared_ptr to itself. As it knows these objects wouldn't live longer than him, it passes a hub_ptr to them (it's just a template wrapper to a regular pointer).

Sharing objects owned via smart pointer

It seems that you correctly separate the concerns of ownership and usage. The only trouble you have is how to forward the components to the rest of your system.

I would keep your owning structure, and create dedicated structures for the specific users:

struct CoreComponents {
unique_ptr<A> a; unique_ptr<B> b; ...
};

struct PartOfTheSystem {
void use(A& a, B& b);
};

struct Game {
CoreComponents components;
PartOfTheSystem user;

void stuff() {
user.use(*components.a, *components.b);
}
};

Yes: more typing.

But also: very clear logic: construction/ownership and use are separate concerns, and this is perfectly clear by design! Also refer to https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-smartptrparam.

Manual Object Ownership vs Smart Pointers

It's not the creation you should worry about, it's the deletion.

With smart pointers (the reference counting kind), objects can be commonly owned be several other objects, and when the last reference goes out of scope, the object is deleted automatically. This way, you won't have to manually delete anything anymore, you can only leak memory when you have circular dependencies, and your objects are never deleted from elsewhere behind your back.

The single-owner-only type (std::auto_ptr) also relieves you of your deleting duty, but it only allows one owner at a time (though ownership can be transferred). This is useful for objects that you pass around as pointers, but you still want them automatically cleaned up when they go out of scope (so that they work well in containers, and the stack unrolling in the case of an exception works as expected).

In any case, smart pointers make ownership explicit in your code, not only to you and your teammates, but also to the compiler - doing it wrong is likely to produce either a compiler error, or a runtime error that is relatively easy to catch with defensive coding. In manually memory-managed code, it is easy to get the ownership situation wrong somewhere (due to misreading comments, or assuming things the wrong way), and the resulting bug is typically hard to track down - you'll leak memory, overwrite stuff that's not yours, the program crashes at random, etc.; these all have in common that the situation where the bug occurs is unrelated to the offending code section.

Can smart pointers be implicitly used as pointers?

NO! It would be a terrible API. Yes, you could easily implement it within shared_ptr, but just because you could doesn't mean you should.

Why is it such a bad idea? The plain-pointer-based interface of bar doesn't retain an instance of the shared pointer. If bar happens to store the raw pointer somewhere and then exit, there's nothing that guarantees that the pointer it had stored won't become dangling in the future. The only way to guarantee that would be to retain an instance of the shared pointer, not the raw pointer (that's the whole point of shared_ptr!).

It gets worse: the following code is undefined behavior if foo() returns a pointer instance that had only one reference when foo() returned (e.g. if foo is a simple factory of new objects):

AnotherClass *ptr = m.foo().get();
// The shared_ptr instance returned by foo() is destroyed at this point
m.bar(ptr); // undefined behavior: ptr is likely a dangling pointer here

Here are the options; consider those listed earlier first before considering their successors.

  • If bar(AnotherClass *) is an external API, then you need to wrap it in a safe way, i.e. the code that would have called Original::bar should be calling MyWrapped::bar, and the wrapper should do whatever lifetime management is necessary. Suppose that there is startUsing(AnotherClass *) and finishUsing(AnotherClass *), and the code expects the pointer to remain valid between startUsing and finishUsing. Your wrapper would be:

    class WithUsing {
    std::unique_ptr<AnotherClass> owner; /* or shared_ptr if the ownership is shared */
    std::shared_ptr<User> user;
    public:
    WithUsing(std::unique_ptr<AnotherClass> owner, std::Shared_ptr<User> user) :
    owner(std::move(owner)), user(std::move(user)) {
    user.startUsing(owner.get());
    }
    void bar() const {
    user.bar(owner.get());
    }
    ~WithUsing() {
    user.finishUsing(owner.get());
    }
    };

    You would then use WithUsing as a handle to the User object, and any uses would be done through that handle, ensuring the existence of the object.

  • If AnotherClass is copyable and is very cheap to copy (e.g. it consists of a pointer or two), then pass it by value:

    void bar(AnotherClass)
  • If the implementation of bar doesn't need to change the value, it can be defined to take a const-value (the declaration can be without the const as it doesn't matter there):

    void bar(const AnotherClass a) { ... }
  • If bar doesn't store a pointer, then don't pass it a pointer: pass a const reference by default, or a non-const reference if necessary.

    void bar(const AnotherClass &a);
    void bar_modifies(AnotherClass &a);
  • If it makes sense to invoke bar with "no object" (a.k.a. "null"), then:

    1. If passing AnotherClass by value is OK, then use std::optional:

      void bar(std::optional<AnotherClass> a);
    2. Otherwise, if AnotherClass takes ownership, passing unique_ptr works fine since it can be null.

    3. Otherwise, passing shared_ptr works fine since it can be null.

  • If foo() creates a new object (vs. returning an object that exists already), it should be returning unique_ptr anyway, not a shared_ptr. Factory functions should be returning unique pointers: that's idiomatic C++. Doing otherwise is confusing, since returning a shared_ptr is meant to express existing shared ownership.

    std::unique_ptr<AnotherClass> foo();
  • If bar should take ownership of the value, then it should be accepting a unique pointer - that's the idiom for "I'm taking over managing the lifetime of that object":

    void bar(std::unique_ptr<const AnotherClass> a);
    void bar_modifies(std::unique_ptr<AnotherClass> a);
  • If bar should retain shared ownership, then it should be taking shared_ptr, and you will be immediately converting the unique_ptr returned from foo() to a shared one:

    struct MyClass {
    std::unique_ptr<AnotherClass> foo();
    void bar(std::shared_ptr<const AnotherClass> a);
    void bar_modifies(std::shared_ptr<AnotherClass> a);
    };

    void test() {
    MyClass m;
    std::shared_ptr<AnotherClass> p{foo()};
    m.bar(p);
    }

shared_ptr(const Type) and shared_ptr(Type) will share the ownership,
they provide a constant view and a modifiable view of the object, respectively. shared_ptr<Foo> is also convertible to shared_ptr<const Foo> (but not the other way round, you'd use const_pointer_cast for that (with caution). You should always default to accessing objects as constants, and only working with non-constant types when there's an explicit need for it.

If a method doesn't modify something, make it self-document that fact by having it accept a reference/pointer to const something instead.



Related Topics



Leave a reply



Submit