
Question:
I was reading the latest Overload (<a href="http://accu.org/var/uploads/journals/Overload120.pdf" rel="nofollow">link</a>) and decided to test out the statement at page 8:
<blockquote>shared_ptr will properly invoke B’s destructor on scope exit, even though the destructor of A is not virtual.
</blockquote>I am using Visual Studio 2013, compiler v120:
#include <memory>
#include <iostream>
struct A {
~A() { std::cout << "Deleting A"; }
};
struct B : public A
{
~B() { std::cout << "Deleting B"; }
};
int main()
{
std::shared_ptr<A> ptr = std::make_shared<B>();
ptr.reset();
return 0;
}
This works as expected and prints out "Deleting BDeleting A"
The article seems to imply that this should also work with std::unique_ptr:
<blockquote>There is almost no need to manage your own resources so resist the temptation to implement your own copy/assign/move construct/move assign/destructor functions.
Managed resources can be resources inside your class definition or instances of your classes themselves. Refactoring the code around standard containers and class templates like unique_ptr or shared_ptr will make your code more readable and maintainable.
</blockquote>However, when changing
std::shared_ptr<A> ptr = std::make_shared<B>();
to
std::unique_ptr<A> ptr = std::make_unique<B>();
the program will only output "Deleting A"
Did I misunderstand the article and the behavior is intended by the standard? Is it a bug with the MSVC compiler?
Answer1:shared_ptr
and unique_ptr
are different. make_shared
will create a type erased deleter object on invocation while with unique_ptr the deleter is part of the type. Therefore the shared_ptr knows the real type when it invokes the deleter but the unique_ptr
doesn't. This makes unique_ptr
's much more efficient which is why it was implemented this way.
I find the article a bit misleading actually. I do not consider it to be good advice to expose copy constructors of a base class with virtual functions, sounds like a lot of slicing problems to me.
Consider the following situation:
struct A{
virtual void foo(){ std::cout << "base"; };
};
struct B : A{
virtual void foo(){ std::cout << "derived"; };
};
void bar(A& a){
a.foo(); //derived
auto localA = a; //poor matanance porgrammer didn't notice that a is polymorphic
localA.foo(); //base
}
I would personally advocate non-intrusive polymorphism <a href="http://isocpp.org/blog/2012/12/value-semantics-and-concepts-based-polymorphism-sean-parent" rel="nofollow">http://isocpp.org/blog/2012/12/value-semantics-and-concepts-based-polymorphism-sean-parent</a> for any new higherarchies, it sidesteps the problem entirely.
Answer2:This behaviour is according to the standard.
The unique_ptr is designed to have no performance overhead compared to the ordinary new/delete.
The shared_ptr has the allowance to have the overhead, so it can be smarter.
According to the standard [20.7.1.2.2, 20.7.2.2.2], the unique_ptr calls delete on the pointer returned by get(), while the shared_ptr deletes the real object it holds - it remembers the proper type to delete (if properly initialized), even without a virtual destructor.
Obviously, the shared_ptr is not all-knowing, you can trick it to behave badly by passing a pointer to the base object like this:
std::shared_ptr<Base> c = std::shared_ptr<Base> { (Base*) new Child() };
But, that would be a silly thing to do anyhow.
Answer3:You <em>could</em> do something similar with unique_ptr
, but since its deleter type is determined statically, you need to statically maintain the proper deleter type. i.e. (<a href="http://coliru.stacked-crooked.com/a/af5f44996ca3b1c2" rel="nofollow">Live demo at Coliru</a>):
// Convert given pointer type to T and delete.
template <typename T>
struct deleter {
template <typename U>
void operator () (U* ptr) const {
if (ptr) {
delete static_cast<T*>(ptr);
}
}
};
// Create a unique_ptr with statically encoded deleter type.
template <typename T, typename...Args>
inline std::unique_ptr<T, deleter<T>>
make_unique_with_deleter(Args&&...args) {
return std::unique_ptr<T, deleter<T>>{
new T(std::forward<Args>(args)...)
};
}
// Convert a unique_ptr with statically encoded deleter to
// a pointer to different type while maintaining the
// statically encoded deleter.
template <typename T, typename U, typename W>
inline std::unique_ptr<T, deleter<W>>
unique_with_deleter_cast(std::unique_ptr<U, deleter<W>> ptr) {
T* t_ptr{ptr.release()};
return std::unique_ptr<T, deleter<W>>{t_ptr};
}
// Create a unique_ptr to T with statically encoded
// deleter for type U.
template <typename T, typename U, typename...Args>
inline std::unique_ptr<T, deleter<U>>
make_unique_with_deleter(Args&&...args) {
return unique_with_deleter_cast<T>(
make_unique_with_deleter<U>(std::forward<Args>(args)...)
);
}
It's a bit awkward to use:
std::unique_ptr<A, deleter<B>> foo = make_unique_with_deleter<A, B>();
auto
improves the situation:
auto bar = make_unique_with_deleter<A, B>();
It doesn't really buy you much, since the dynamic type is encoded right there in the static type of the unique_ptr
. If you're going to carry the dynamic type around, why not simply use unique_ptr<dynamic_type>
? I conjecture such a thing may have some use in generic code, but finding an example of such is left as an exercise to the reader.
The technique std::shared_ptr
employs to do the magic is called <a href="http://www.cplusplus.com/articles/oz18T05o/" rel="nofollow">type erasure</a>. If you are using gcc, try to find the file bits/shared_ptr_base.h
and check the implementation. I'm using gcc 4.7.2.
unique_ptr
is designed for minimum overhead and does not use type erasure to remember the actual type of the pointer it holds.
Here is a great discussion on this topic: <a href="https://stackoverflow.com/a/22861890/930095" rel="nofollow">link</a>
<hr />EDIT: a simple implementation of shared_ptr
to showcase how type erasure is achieved.
#include <cstddef>
// A base class which does not know the type of the pointer tracking
class ref_counter_base
{
public:
ref_counter_base() : counter_(1) {}
virtual ~ref_counter_base() {}
void increase()
{
++counter_;
}
void release()
{
if (--counter_ == 0) {
destroy();
delete this;
}
}
virtual void destroy() = 0;
private:
std::size_t counter_;
};
// The derived class that actually remembers the type of
// the pointer and deletes the pointer on destroy.
template <typename T>
class ref_counter : public ref_counter_base
{
public:
ref_counter(T *p) : p_(p) {}
virtual void destroy()
{
delete p_;
}
private:
T *p_;
};
template <typename T>
class shared_ptr
{
public:
shared_ptr(T *p)
: p_(p)
, counter_(new ref_counter<T>(p))
{
}
// Y* should be implicitely convertable to T*,
// i.e. Y is derived from T
template <typename Y>
shared_ptr(Y &other)
: p_(other.get())
, counter_(other.counter())
{
counter_->increase();
}
~shared_ptr()
{
counter_->release();
}
T* get() { return p_; }
ref_counter_base* counter() { return counter_; }
private:
T *p_;
ref_counter_base *counter_;
};