What Is Shared_Ptr's Aliasing Constructor For

What is shared_ptr's aliasing constructor for?

Simple example:

struct Bar { 
// some data that we want to point to
};

struct Foo {
Bar bar;
};

shared_ptr<Foo> f = make_shared<Foo>(some, args, here);
shared_ptr<Bar> specific_data(f, &f->bar);

// ref count of the object pointed to by f is 2
f.reset();

// the Foo still exists (ref cnt == 1)
// so our Bar pointer is still valid, and we can use it for stuff
some_func_that_takes_bar(specific_data);

Aliasing is for when we really want to point to Bar, but we also don't want the Foo to get deleted out from under us.


As Johannes points out in the comments, there is a somewhat equivalent language feature:

Bar const& specific_data = Foo(...).bar;
Bar&& also_specific_data = Foo(...).bar;

We're taking a reference to a member of a temporary, but the temporary Foo is still kept alive as long as specific_data is. As with the shared_ptr example, what we have is a Bar whose lifetime is tied to a Foo - a Foo that we cannot access.

shared_ptr aliasing constructor

Example:

#include <iostream>
#include <iomanip>

struct some_type
{
int i;
};

void my_deleter(some_type* p)
{
std::cout << "my_deleter called!" << std::endl;
delete p;
}

#include <memory>
int main()
{
std::shared_ptr<int> pm;

{
// Note: better use make_shared
auto x = new some_type;
// create a shared_ptr that owns x and a deleter
std::shared_ptr<some_type> r(x, &my_deleter);
std::cout << r.use_count() << std::endl;

// share ownership of x and the deleter with pm
pm = std::shared_ptr<int>(r, &r->i);
std::cout << r.use_count() << std::endl;

// r gets destroyed
}
std::cout << pm.use_count() << std::endl;
std::cout << "get_deleter == 0? " << std::boolalpha
<< (nullptr == std::get_deleter<decltype(&my_deleter)>(pm))
<< std::endl;
}

Output:


1
2
1
get_deleter == 0? false
my_deleter called!

N.B. I can't compile this example with a free function my_deleter, there's some casting error for the free get_deleter function (trying to cast from void* to a function pointer type with a static_cast).


Aliasing ctor:
[util.smartptr.shared.const]/13-14

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

13 Effects: Constructs a shared_ptr instance that stores p and shares ownership with r.

14 Postconditions: get() == p && use_count() == r.use_count()

Ctor with user-provided deleter:
[util.smartptr.shared.const]/9

template shared_ptr(Y* p, D d);

Effects: Constructs a shared_ptr object that owns the object p and the deleter d.

Dtor:
[util.smartptr.shared.dest]/1

~shared_ptr();

1 Effects:

  • If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
  • Otherwise, if *this owns an object p and a deleter d, d(p) is called.
  • Otherwise, *this owns a pointer p, and delete p is called.

Combining those (let's skip the assignment operators):

  • The shared_ptr instance r owns both the object and the deleter.
  • The aliasing ctor lets the new shared_ptr instance share ownership with r (i.e. for both, the object and the deleter).
  • When the dtor of this new instance is called (or an assignment operator),
    • If use_count > 1, no effects.
    • Else, this instance owns the object which r pointed to and the deleter (if any) and will either use this deleter (if it exists) or delete on the object pointed to.

Understanding the prototype of shared_ptr aliasing constructor

You are confusing different template parameters/arguments. _Yp is not the template parameter we are instantiating the shared_ptr with. The parameter of the whole shared_ptr template in GCC's implementation is called _Tp, not _Yp. Inside shared_ptr that _Tp is also known as element_type.

Meanwhile, _Yp is a parameter of a nested member template, which is constructor template.

shared_ptr itself and its constructor template are two "orthogonal" templates. _Tp and _Yp are two independent and unrelated template parameters.

You don't (and can't) explicitly specify the argument for _Yp. It will be deduced automatically. But you have to specify the argument for _Tp, which is exactly what you see in your example

shared_ptr<int> pi(pii, &pii->first);
^ ^
| |
| The `_Yp` parameter is kinda/sorta implicitly present here.
| It parametrizes the constructor template. C++ has no syntax
| for specifying it explicitly
|
This is `_Tp`, not `_Yp`. `_Tp` parametrizes
the whole `shared_ptr` template

The _Tp in this example is specified as int, as it should be. The _Yp is deduced from pii as pair<int,int>, exactly as you expected it to be.

Using C++ shared pointer's aliasing constructor with an empty shared pointer

As you already know, with your current solution, p has a use_count() of zero, that's why the weak_ptr is expired. This seems to be ok, according to the C++ draft N4296:

20.8.2.2.1 shared_ptr constructors [util.smartptr.shared.const]

template shared_ptr(const shared_ptr& r, T* p) noexcept;

13 Effects: Constructs a shared_ptr instance that stores p and shares ownership with r.

14 Postconditions: get() == p && use_count() == r.use_count()

15 [ Note: To avoid the possibility of a dangling pointer, the user of this constructor must ensure that p
remains valid at least until the ownership group of r is destroyed. — end note ]

16 [ Note: This constructor allows creation of an empty shared_ptr instance with a non-null stored
pointer. — end note ]

20.8.2.2.2 shared_ptr destructor [util.smartptr.shared.dest]

~shared_ptr();

1 Effects:

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

(1.2) — Otherwise, if *this owns an object p and a deleter d, d(p) is called.

(1.3) — Otherwise, *this owns a pointer p, and delete p is called

emphasis mine.

You could use the following instead which gives a shared_ptr with a use_count() of one:

std::shared_ptr<int> p(&global, [](int*){});

This uses an empty custom deleter.

Why doesn't the shared pointer aliasing constructor use pass-by-value semantics

You are missing the destructor. your suggested implementation will decrement the count upon return from constructor (the automatic shared_ptr instance will be destructed). So if the compiler is not able to optimize to noop, you just pay the cost of two extra atomic operation for no gain and an added logical error that will backlash with a UB.

Regardless of the devised semantics, the constructor must increment the count. However, your proposed solution -after fixing the bug you've introduced- combines the implementation of both r-value and l-value versions of the constructor in a by-value constructor, discarding the mandatory atomic access optimization imposed by current implementation.

Is alias construction of shared_ptr from void safe?

From https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr:

... such as in the typical use cases where ptr is a member of the object managed by r or is an alias (e.g., downcast) of r.get() ...

So this is exactly the envisioned use case.

How to implement the aliasing constructor by own for my own shared_ptr implementation?

Boost solves this problem by either using template member friends or making the members public, depending on the value of the define BOOST_NO_MEMBER_TEMPLATE_FRIENDS.

For example the friend declaration looks like this:

template<class Y> friend class shared_ptr;
template<class Y> friend class weak_ptr;

How would one specify a custom deleter for a shared_ptr constructed with an aliasing constructor?

It doesn't really make sense to add a custom deleter in the construction of the aliasing shared_ptr.

The deleter is associated with the managed object, not the specific shared_ptr instance that will actually call it. The deleter is stored together with the object in the shared control block.

So once all aliasing and non-aliasing shared_ptr instances to the shared object are destroyed, the last instance (whether it is aliasing or not) will call the deleter which was associated with the object when it was first put under shared_ptr control.

So to add a custom deleter for the Foo object, replace std::make_shared:

auto f = std::shared_ptr<Foo>(new Foo(some, args, here), some_deleter_here);

some_deleter_here will then be called with the Foo pointer as argument, even if the aliasing shared_ptr is the last to be destroyed.

Note however that if you are using a custom deleter, then you probably are not creating the pointer with a simple call to new. The only correct way to delete an object created with new is to call delete on it, which is what the default deleter already does.



Related Topics



Leave a reply



Submit