Differencebetween an Empty and a Null Std::Shared_Ptr in C++

What is the difference between an empty and a null std::shared_ptr in C++?

It's a weird corner of shared_ptr behavior. It has a constructor that allows you to make a shared_ptr that owns something and points to something else:

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, T *ptr );

The shared_ptr constructed using this constructor shares ownership with r, but points to whatever ptr points to (i.e., calling get() or operator->() will return ptr). This is handy for cases where ptr points to a subobject (e.g., a data member) of the object owned by r.

The page you linked calls a shared_ptr that owns nothing empty, and a shared_ptr that points to nothing (i.e., whose get() == nullptr) null. (Empty is used in this sense by the standard; null isn't.) You can construct a null-but-not-empty shared_ptr, but it won't be very useful. An empty-but-not-null shared_ptr is essentially a non-owning pointer, which can be used to do some weird things like passing a pointer to something allocated on the stack to a function expecting a shared_ptr (but I'd suggest punching whoever put shared_ptr inside the API first).

boost::shared_ptr also has this constructor, which they call the aliasing constructor.

std::shared_ptr which is null but not empty

Yes, the "null but not empty" shared_ptr will keep the object alive because it shares ownership with the shared_ptr it was constructed from. All shared_ptrs that share ownership with each other contribute to an atomic reference count stored in the control block, and only when this reference count hits zero is the owned object destroyed.

For standardese, see [util.smartptr.shared.const]/14:

template<class Y> shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;

Constructs a shared_ptr instance that stores p and shares ownership with r

There are no constraints specified on the value of p; it may therefore be any valid pointer value, including null or even a past-the-end pointer (though I'm not sure why you'd want to do this).

Then see [util.smartptr.shared.dest]/(1.1):

If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.

In other words, when ps is destroyed, it still shares ownership with p, so the object is not destroyed yet.

The resulting object is not really unreachable in the usual sense, since it is still possible to destroy it. You just can't do anything else with it.

Is it correct to return null shared_ptr?

Is it correct to return shared_ptr implicitly initialized with nullptr like in upper example?

Yes, it is correct to initialize shared_ptr with nullptr. It is also correct to assign nullptr to shared_ptr.

Or should I return shared_ptr default constructed instead?

You can do this in both ways:

  1. returning shared_ptr initialized with nullptr

    return shared_ptr<Object>(nullptr);
  2. returning shared_ptr default constructed.

    return shared_ptr<Object>();

Both ways are correct and both have the same effect. You can use whatever way you want.

What in case it would be weak_ptr? What is proper way to check that empty weak_ptr has been returned? by weak_ptr::expired function or are there other ways?

weak_ptr becomes nullptr (expires) whenever the last shared_ptr associated with object is destroyed.

The proper way to work with weak_ptr is to convert it to shared_ptr with lock method, and then to work with created shared_ptr. In that case your weak_ptr will no expire until you have that new shared_ptr. If you don't convert weak_ptr into shared_ptr, your weak_ptr may expire at any moment.

And yes, before working with newly created shared_ptr, you must check that it isn't null, because weak_ptr may had been expired before you created shared_ptr with lock method.

std::weak_ptr<Object> Storage::findObject();

...

std::weak_ptr <Object> weak = Storage::findObject();
std::shared_ptr<Object> shared = weak.lock();
if (shared) // check that weak was not expired when we did "shared = weak.lock()"
{
// do something with shared, it will not expire.
}

How to test whether a shared_ptr is empty or owns nothing

The static instance of shared_ptr will hold a reference, so the object will always have a ref count >= 1, and won't be deleted until static cleanup happens. As cnettel says in the comments, you need std::weak_ptr here.

weak_ptr is basically a shared_ptr which doesn't contribute to the ref count. It has an atomic cast to std::shared_ptr via the .lock() method. The resulting std::shared_ptr will convert to false if it is not initialized, so you know to reinitialize (or intialize for the first time).

Example:

std::shared_ptr<Obj> instance() {
static std::weak_ptr<Obj> instance;
static std::mutex lock;

std::lock_guard<std::mutex> guard(lock);

auto result = instance.lock();
if (!result) {
result = std::make_shared<Obj>();
instance = result;
}

return result;
}

Why does one need a null shared_ptr and how can it be used?

There is no need to use that hack to get a null (empty) shared_ptr. Simply use the default constructor:

std::shared_ptr<Investment> pInv; // starts null

To assign a pointer to a shared_ptr, either do it at construction time:

std::shared_ptr<Investment> pInt(new Investment);
// not allowed due to explicit annotation on constructor:
// std::shared_ptr<Investment> pInt = new Investment;

Or use the .reset() function:

pInt.reset(new Investment);

It's possible that the author of that article may have intended to provide a custom deleter (getRidOfInvestment). However, the deleter function is reset when .reset() is called, or when otherwise the inner pointer is changed. If you want a custom deleter, you must pass it to .reset() upon creation of the shared_ptr.

One pattern you might want to use to make this more foolproof is a custom creation function:

class Investment {
protected:
Investment();
// ...
public:
static shared_ptr<Investment> create();
};

shared_ptr<Investment> Investment::create() {
return shared_ptr<Investment>(new Investment, getRidOfInvestment);
}

Later:

shared_ptr<Investment> pInv = Investment::create();

This ensures you will always have the correct destructor function attached to the shared_ptrs created from Investments.

Semantic of empty shared_ptr

The semantics are:

  • If you default-construct a shared pointer, or construct one from nullptr_t, it's empty; that is, it doesn't own any pointer.
  • If you construct one from a raw pointer, it takes ownership of that pointer, whether or not it's null. I guess that's done for the reason you mention (avoiding a runtime check), but I can only speculate about that.

So your example isn't empty; it owns a null pointer.

Does it make sense to check for nullptr in custom deleter of shared_ptr?

The constructor std::shared_ptr<T>::shared_ptr(Y*p) has the requirement that delete p is a valid operation. This is a valid operation when p equals nullptr.

The constructor std::shared_ptr<T>::shared_ptr(Y*p, Del del) has the requirement that del(p) is a valid operation.

If your custom deleter cannot handle p being equal to nullptr then it is not valid to pass a null p in the constructor of shared_ptr.

The constructor you offer as an example can be better presented, thus:

#include <memory>

struct MyClass {
void open() {
// note - may throw
};

void close() noexcept {
// pre - is open
}
};

struct Closer
{
void operator()(MyClass* p) const noexcept
{
p->close();
delete p; // or return to pool, etc
}
};

auto CreateMyClass() -> std::unique_ptr<MyClass, Closer>
{
// first construct with normal deleter
auto p1 = std::make_unique<MyClass>();

// in case this throws an exception.
p1->open();

// now it's open, we need a more comprehensive deleter
auto p = std::unique_ptr<MyClass, Closer> { p1.release(), Closer() };
return p;
}

int main()
{
auto sp = std::shared_ptr<MyClass>(CreateMyClass());
}

Note that it is now not possible for the shared_ptr to own a null object.

Is there a recommended way to test if a smart pointer is null?

Is there a difference between doing

 std::shared_ptr<int> p;
if (!p) { // method 1 }
if (p == nullptr) { // method 2 }

No, there's no difference. Either of that operations has a properly defined overload.

Another equivalent would be

 if(p.get() == nullptr)


Related Topics



Leave a reply



Submit