Shared_Ptr Magic :)

shared_ptr magic :)

Yes, it is possible to implement shared_ptr that way. Boost does and the C++11 standard also requires this behaviour. As an added flexibility shared_ptr manages more than just a reference counter. A so-called deleter is usually put into the same memory block that also contains the reference counters. But the fun part is that the type of this deleter is not part of the shared_ptr type. This is called "type erasure" and is basically the same technique used for implementing the "polymorphic functions" boost::function or std::function for hiding the actual functor's type. To make your example work, we need a templated constructor:

template<class T>
class shared_ptr
{
public:
...
template<class Y>
explicit shared_ptr(Y* p);
...
};

So, if you use this with your classes Base and Derived ...

class Base {};
class Derived : public Base {};

int main() {
shared_ptr<Base> sp (new Derived);
}

... the templated constructor with Y=Derived is used to construct the shared_ptr object. The constructor has thus the chance to create the appropriate deleter object and reference counters and stores a pointer to this control block as a data member. If the reference counter reaches zero, the previously created and Derived-aware deleter will be used to dispose of the object.

The C++11 standard has the following to say about this constructor (20.7.2.2.1):

Requires: p must be convertible to T*. Y shall be a complete type. The expression delete p shall be well formed, shall have well defined behaviour and shall not throw exceptions.

Effects: Constructs a shared_ptr object that owns the pointer p.

And for the destructor (20.7.2.2.2):

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, if *this owns a pointer p, and delete p is called.

(emphasis using bold font is mine).

Collection specialized for shared_ptr

that is aware of shared_ptr internals,

That should answer your question right there. To be aware of the internals, such a collection would almost certainly have to be part of boost's smart pointer libraries. Unfortunately, there is no such thing.

This is indeed a downside to smart pointers. I would recommend using data structures that limit the number of copies done internally. Vector's reallocations will be painful. Perhaps a deque, which has a chunked based allocation, would be useful. Keep in mind too that vector implementations tend to get new memory in exponentially increasing chunks. So they don't reallocate, say, every 10 elements. Instead you might start out with 128 elements, then the vector reserves itself 256, then moves up to 512, 1024, etc. Each time doubling what is needed.

Short of this there's boost's ptr_vector or preallocating your data structures with enough space to prevent internal copying.

Forwarding a shared_ptr without class declaration

You need at least a forward declaration for T for every mention of shared_ptr<T>.

Only if you use unary shared_ptr::operator* and shared_ptr::operator->, the full thing is needed. Under the hood, shared_ptr uses a mix of compiletime- and runtime-polymorphism, making this possible. See also this question to learn about the "magic".

Example:

// frob.h
#ifndef FROB_H
#define FROB_H

#include <shared_ptr>

class Foo;
void grind (std::shared_ptr<Foo>);

#endif

Note that the canonical way to pass shared_ptr is by value (i.e. remove the const&).

How do `shared_ptr`s achieve covariance?

Yes, specializations of the same class template by default have almost no relationship and are essentially treated like unrelated types. But you can always define implicit conversions between class types by defining converting constructors (To::To(const From&)) and/or conversion functions (From::operator To() const).

So what std::shared_ptr does is define template converting constructors:

namespace std {
template <class T>
class shared_ptr {
public:
template <class Y>
shared_ptr(const shared_ptr<Y>&);
template <class Y>
shared_ptr(shared_ptr<Y>&&);
// ...
};
}

Though the declaration as shown would allow conversions from any shared_ptr to any other, not just when the template argument types are compatible. But the Standard also says about these constructors ([util.smartptr]/5 and [util.smartptr.const]/18 and util.smartptr.const]/21):

For the purposes of subclause [util.smartptr], a pointer type Y* is said to be compatible with a pointer type T* when either Y* is convertible to T* or Y is U[N] and T is cv U[].

The [...] constructor shall not participate in overload resolution unless Y* is compatible with T*.

Although this restriction could be done in any way, including compiler-specific features, most implementations will enforce the restriction using an SFINAE technique (Substitution Failure Is Not An Error). One possible implementation:

#include <cstddef>
#include <type_traits>

namespace std {
template <class Y, class T>
struct __smartptr_compatible
: is_convertible<Y*, T*> {};

template <class U, class V, size_t N>
struct __smartptr_compatible<U[N], V[]>
: bool_constant<is_same_v<remove_cv_t<U>, remove_cv_t<V>> &&
is_convertible_v<U*, V*>> {};

template <class T>
class shared_ptr {
public:
template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
shared_ptr(const shared_ptr<Y>&);

template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
shared_ptr(shared_ptr<Y>&&);

// ...
};
}

Here the helper template __smartptr_compatible<Y, T> acts as a "trait": it has a static constexpr member value which is true when the types are compatible as defined, or false otherwise. Then std::enable_if is a trait which has a member type called type when its first template argument is true, or does not have a member named type when its first template argument is false, making the type alias std::enable_if_t invalid.

So if template type deduction for either constructor deduces the type Y so that Y* is not compatible with T*, substituting that Y into the enable_if_t default template argument is invalid. Since that happens while substituting a deduced template argument, the effect is just to remove the entire function template from consideration for overload resolution. Sometimes an SFINAE technique is used to force selecting a different overload instead, or as here (most of the time), it can just make the user's code fail to compile. Though in the case of the compile error, it will help that a message appears somewhere in the output that the template was invalid, rather than some error even deeper within internal template code. (Also, an SFINAE setup like this makes it possible for a different template to use its own SFINAE technique to test whether or not a certain template specialization, type-dependent expression, etc. is or isn't valid.)

unique/shared_ptr with custom operator=

You may create wrapper around std::unique_ptr

// To handle your cusstom Destroy
struct DestroyDeleter
{
void operator(Interface* o) {
object->Destroy();
delete object;
}
};

using InterfacePtr = std::unique_ptr<Interface, DestroyDeleter>;

// To handle the copy with clone:
class wrapper
{
public:
explicit wrapper(InterfacePtr o) : data(std::move(o)) {}

wrapper(const wrapper& rhs) : data(rhs.data->Clone()) {}
wrapper(wrapper&& rhs) = default;

wrapper& operator =(const wrapper& rhs) { data = rhs.data->Clone(); }
wrapper& operator =(wrapper&& rhs) = default;

const Interface* operator ->() const { return data.get(); }
Interface* operator ->() { return data.get(); }

const Interface& operator *() const { return data; }
Interface& operator *() { return *data; }

private:
InterfacePtr data;
};

why shared pointer has a virtual function

Is this not supposed to be the reponsibility of the class of that pointed object to have a virtual destructor?

That would be one possible way to design a shared pointer, but std::shared_ptr allows you to do the following, even if Base does not have a virtual destructor:

std::shared_ptr<Base> p { new Derived{} };

It does this by capturing the correct deleter for the argument when the std::shared_ptr is constructed, then calls that when the reference count hits zero rather than just using delete (of course, you can pass your own custom deleter to use instead). This is commonly referred to as type erasure, and this technique is generally implemented using virtual function calls.

Why std::shared_ptr calls destructors from base and derived classes, where delete calls only destructor from base class?

delete a is undefined behaviour, because the class Base does not have a virtual destructor and the "complete object" of *a (more accurately: the most-derived object containing *a) is not of type Base.

The shared pointer is created with a deduced deleter that deletes a Derived *, and thus everything is fine.

(The effect of the deduced deleter is to say delete static_cast<Derived*>(__the_pointer)).

If you wanted to reproduce the undefined behaviour with the shared pointer, you'd have to convert the pointer immediately:

// THIS IS AN ERROR
std::shared_ptr<Base> shared(static_cast<Base*>(new Derived));

In some sense, it is The Right Way for the shared pointer to behave: Since you are already paying the price of the virtual lookup for the type-erased deleter and allocator, it is only fair that you don't then also have to pay for another virtual lookup of the destructor. The type-erased deleter remembers the complete type and thus incurs no further overhead.



Related Topics



Leave a reply



Submit