Why unique-ptr doesn't check base class to virtual destructible?
Asked Answered
I

2

20

Consider this example :

#include <cstdio>
#include <memory>

struct base
{
    base( int i ): i(i)    {    printf("base ctor\n"); }
    ~base() {     printf("base non-virtual dtor\n"); } // non-virtual
    int i;
};

struct derived : public base
{
    char* s;
    derived(int i): base(i), s(new char[i] )
    {
        printf("derived ctor\n");
    }
    ~derived()
    {
        printf("derived dtor\n");
        delete [] s;
    }
};

int main()
{
    printf("Success\n");

    //raw pointer
    printf("RAW-POINTER\n");
    {
        base* b = new derived(2);
         // ......
        delete b; //here memory leak, but it's old- and error-prone code.
    }
    printf("---END-RAW-POINTER--\n");

    //unique-ptr
    printf("UNIQUE_PTR\n");
    {
       // I would that, this doesn't compile, because base- has not virtual destructor.
        std::unique_ptr<base> bu( new derived(3) ); // here still memory leak !!!!
    }
    printf("--END-UNIQUE_PTR--\n");


    return 0;
}

The code std::unique_ptr<base> bu( new derived(3) ); easy prohibit with std::has_virtual_destructor type-traits. Live code

So why does above code compiled? Is this allowed by standard ?

EDIT: interesting, but with std::shared_ptr worked, i.e. both base and derived dtor will call:

    printf("SHARED_PTR\n");
    {
        std::shared_ptr<base> b(new derived(3));
    }
    printf("--END-SHARED_PTR--\n");

Output:
SHARED_PTR
base ctor
derived ctor
derived dtor
base non-virtual dtor
--END-SHARED_PTR--

Why std::shared_ptr can call dervied class dtor, but std::unique_ptr can't ???

EDIT2: simple I need something like:

template< typename T, typename D = default_deleter<T> >
class unique_ptr{
  .............

  template< typename U >
  unique_ptr( U* u ) if ( U != T && T - is class && T is base of U, and D - is default_deleter, and T - has not virtual destructor ) then = delete this ctor.

};
Inky answered 2/3, 2014 at 7:21 Comment(4)
@Brian Bi: Not all used custom deleter, in many cases users limits with default_deleter.Inky
std::shared_ptr will work. It will call the destructor of base and derived: ideone.com/Nc542VImpose
If you are using public inheritance, you should define destructor as virtual. (#271417)Loam
Deleting a pointer p where the static type of *p is different from the dynamic type is Undefined Behaviour, but not forbidden (= not ill-formed). Also, you'd have to check in the dtor of unique_ptr, as you can supply incomplete types as long as they're complete at the point of instantiation of the dtor of unique_ptr (or their dtor is trivial).Plead
C
18

The difference between unique_ptr and shared_ptr is in the language of the standard, with respect to their destructors (and constructors). That language for the deleters of both smart pointers, which applies to your example, is similar but subtly different:

[20.7.1.2.2] unique_ptr destructor 
... If get() ==  nullptr there are no efects. Otherwise get_deleter()(get()).

[20.7.2.2.2] shared_ptr destructor
... if *this owns an object p and a deleter d, d(p) is called.

You can see that in both cases the standard says to call the deleter, the difference though in how the deleter is decided on, and that unique_ptr deletes the pointer it obtains via get(), while shared_ptr deletes the object. This distinction is important. Look at how the constructors for both classes are also different:

The shared_ptr is defined as follows:

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

While the unique_ptr explicit single argument constructor is,

template <class  T, class D = default_delete<T>>
class  unique_ptr  {
...
    explicit  unique_ptr(pointer  p)  noexcept;
...

Observe that unique_ptr just gets the default delete for the type, that would be the plain delete in your case, and stores the pointer. However, shared_ptr<T> constructor is not templated on T (!), it is templated on the type of the object Y that it is constructed with. Thus, in your scenario,

std::shared_ptr<base> b(new derived(3));

the shared_ptr will be constructed with T=base but Y=derived, allowing to explicitly destroy the derived object, and not leaking memory in your example.


While you cannot change the standard, what you can do is either inherit from unique_ptr in your project, or provide your own wrappers to enforce the desired behaviour. For example,

namespace {
    template <class  T>
    struct checked_delete : public std::default_delete<T> {
        static_assert(std::has_virtual_destructor<T>::value, "");
    };    

    template <class T, class D = checked_delete<T>, class U>  
    std::unique_ptr<T, D>
    make_unique_ptr(U* p) { return std::unique_ptr<T, D>(p, D()); }    
}

// now this won't compile, because checked_delete<base> will not compile:
auto bu = make_unique_ptr<base>(new derived(3));
Ciera answered 2/3, 2014 at 12:1 Comment(2)
your last example is very nice solution, but I need checking virtual dtor only when it constructed from derived class, not a every case. Hovewer, Thanks a lot. Now, I know how it implement.Inky
I'm not certain about the first paragraph, get_deleter()(the stored object pointer) would not work anyway because it still pass as base class.Seline
P
5

Not all unique_pointers are used polymorphically:

std::unique_ptr<int> p(new int(42));

This would not compile with the restriction you propose. Same with classes:

std::unique_ptr<YourClassHere> p(new YourClassHere);
Payroll answered 2/3, 2014 at 7:25 Comment(4)
int - is not class type. see is_class: en.cppreference.com/w/cpp/types/is_classInky
Your second example, template parameter unique_ptr and parameter constructor - are same classes. What does happen, if theiy are different classes? In this case, if base class doesn't have virtual desturctor, derived class destructor will not called.Inky
Which is exactly the same behavior you get with raw pointers.Payroll
Raw pointer is error prone and all know it already, but std::unique_ptr - more knew ideome which MAY BE check within some errors in compile time. I asked why std::unique_ptr was not check virtual dtor base class when it constructed from derived calss, which 100% can check.Inky

© 2022 - 2024 — McMap. All rights reserved.