How does the custom deleter of std::unique_ptr work?
Asked Answered
H

4

57

According to N3290, std::unique_ptr accepts a deleter argument in its constructor.

However, I can't get that to work with Visual C++ 10.0 or MinGW g++ 4.4.1 in Windows, nor with g++ 4.6.1 in Ubuntu.

I therefore fear that my understanding of it is incomplete or wrong. I can't see the point of a deleter argument that's apparently ignored, so can anyone provide a working example?

Preferably I'd like to see also how that works for unique_ptr<Base> p = unique_ptr<Derived>( new Derived ).

Possibly with some wording from the standard to back up the example, i.e. that with whatever compiler you're using, it actually does what it's supposed to do?

Hoofbeat answered 25/11, 2011 at 21:39 Comment(0)
F
46

This works for me in MSVC10

int x = 5;
auto del = [](int * p) { std::cout << "Deleting x, value is : " << *p; };
std::unique_ptr<int, decltype(del)> px(&x, del);

And on gcc 4.5, here

I'll skip going to the standard, unless you don't think that example is doing exactly what you'd expect it to do.

Fazeli answered 25/11, 2011 at 21:52 Comment(9)
The gcc link is broken, can someone regerate the code? What is the difference in gcc?Singspiel
@alfC: There is no difference. It's the exact same code as shown in my answer. The link was just an online demonstration of the code compiling and running. I've updated it.Fazeli
Why not just "std::unique_ptr<int, decltype(del)> px(&x);" ?Dynamiter
@Jon: Because lambda types do not have default constructors.Fazeli
@Benjamin: Any idea why it works in VS2013? It's a pity that "del" has to be specified twice, and that we have to create an instance when all we really wanted was a type.Dynamiter
@Jon: Couldn't tell you why it works in VS2013. Either a compiler extension or a bug. What do you mean "we have to create an instance when all we really wanted was a type"? You can't define a lambda type without creating an instance of it.Fazeli
@Benjamin: Yes, I know (why can't we decltype an unevaluated lambda?). My issue is where to put "auto del=..." if I want the unique_ptr to be a class member variable?Dynamiter
Quite pernicious because the pointer isn't deleted, and indeed shouldn't be as it is a reference to a local object.Stewpan
This answer can be confusing for people trying to learn how to use custom deleters, as the decltype trick only works when the object being managed by unique_ptr is being destroyed at the end of the scope where it is created. If your unique_ptr is stored as a member variable or returned from a function, then there is no way to express that decltype, and you need to use std::function there instead.Digamy
B
27

To complement all previous answers, there is a way to have a custom deleter without having to “pollute” the unique_ptr signature by having either a function pointer or something equivalent in it like this:

std::unique_ptr<MyType, myTypeDeleter> // not pretty

This is achievable by providing a full specialization to the std::default_delete class template, like this:

template<>
class std::default_delete<MyType>
{
public:
    void operator()(MyType *ptr) const
    {
        delete ptr;
    }
};

And now all std::unique_ptr<MyType> that “sees” this specialization will be deleted with it. Just be aware that it might not be what you want for all std::unique_ptr<MyType>, so chose carefully your solution.

Burgoo answered 17/5, 2013 at 18:2 Comment(7)
Specializing std templates is legal and not a bad practice, but there are "rules" you need to follow, see this post. std::default_delete is the perfect candidate for template specialization.Burgoo
Perhaps something like: class BIGNUM_ptr : public std::unique_ptr<BIGNUM, decltype(&::BN_free)> { BIGNUM_ptr(BIGNUM b) : std::unique_ptr<BIGNUM, decltype(&::BN_free)>(b, &::BN_free); }; although I haven't got it to work yet, and then I'd want to make the class a template...Brachial
I've added a template alternative unique_dptr that allows the destructor to be specified once in the template rather than each time with the instantiation. (with help from @RSahu) #33418529Brachial
This answer using specialisation of std::default_delete<> is the most idiomatic and practical solution. Awaiting for unwanted 24 hours to be able to award a small "thank you" bounty.Wrack
@Wrack Note that such a specialization is only legal if it calls delete on ptr. Which is not usually what you want. And if you implement it in any other way, then it is illegal.Coleorhiza
@spectras, where does the standard say this? It doesn’t make sense either, as you might have a type whose space is taken care of otherwise, but another form of cleanup is required.Piss
@Piss look up in the standard the mandatory behavior of default_delete, section 20.8.1.1.2. You are allowed to specialize default_delete as long as you implement the mandatory behavior of calling delete on the pointer. This is mostly useful for logging, but not much more. For a better solution, check solidabstractions.com/2019/zero-cost-unique_ptrColeorhiza
H
11

My question has been pretty well answered already.

But just in case people wondered, I had the mistaken belief that a unique_ptr<Derived> could be moved to a unique_ptr<Base> and would then remember the deleter for the Derived object, i.e., that Base would not need to have a virtual destructor. That was wrong. I'd select Kerrek SB's comment as "the answer", except one cannot do that for a comment.

@Howard: the code below illustrates one way to achieve what I believed the cost of a dynamically assigned deleter had to mean that unique_ptr supported out of the box:

#include <iostream>
#include <memory>           // std::unique_ptr
#include <functional>       // function
#include <utility>          // move
#include <string>
using namespace std;

class Base
{
public:
    Base() { cout << "Base:<init>" << endl; }
    ~Base() { cout << "Base::<destroy>" << endl; }
    virtual string message() const { return "Message from Base!"; }
};

class Derived
    : public Base
{
public:
    Derived() { cout << "Derived::<init>" << endl; }
    ~Derived() { cout << "Derived::<destroy>" << endl; }
    virtual string message() const { return "Message from Derived!"; }
};

class BoundDeleter
{
private:
    typedef void (*DeleteFunc)( void* p );

    DeleteFunc  deleteFunc_;
    void*       pObject_;

    template< class Type >
    static void deleteFuncImpl( void* p )
    {
        delete static_cast< Type* >( p );
    }

public:
    template< class Type >
    BoundDeleter( Type* pObject )
        : deleteFunc_( &deleteFuncImpl< Type > )
        , pObject_( pObject )
    {}

    BoundDeleter( BoundDeleter&& other )
        : deleteFunc_( move( other.deleteFunc_ ) )
        , pObject_( move( other.pObject_ ) )
    {}

    void operator() (void*) const
    {
        deleteFunc_( pObject_ );
    }
};

template< class Type >
class SafeCleanupUniquePtr
    : protected unique_ptr< Type, BoundDeleter >
{
public:
    typedef unique_ptr< Type, BoundDeleter >    Base;

    using Base::operator->;
    using Base::operator*;

    template< class ActualType >
    SafeCleanupUniquePtr( ActualType* p )
        : Base( p, BoundDeleter( p ) )
    {}

    template< class Other >
    SafeCleanupUniquePtr( SafeCleanupUniquePtr< Other >&& other )
        : Base( move( other ) )
    {}
};

int main()
{
    SafeCleanupUniquePtr< Base >  p( new Derived );
    cout << p->message() << endl;
}
Hoofbeat answered 26/11, 2011 at 10:38 Comment(3)
can you explain what use it has to have a class with a non-virtual destructor, yet derive from it and wanting to get the derived destructor called through the base pointer? Doesn't this go against common sense about virtual destructors?Febricity
@stijn: As long as some other mechanism (such as a custom deleter) does the job of identifying the most derived class, the destructor doesn't technically need to be virtual. One valid reason for then making it non-virtual, is to retain compatibility with an externally imposed memory layout, i.e. you don't want a vtable ptr in there at the front of the memory region. Another valid reason is that if destruction is always supposed to be through a deleter, then making the destructor virtual would incorrectly indicate that one could use C++ delete, or at least that there was some reason for it.Hoofbeat
@stjn Another reason is where you are using multiple techniques of memory allocation/deallocation, and you only want the creation point to know about the allocation policy/technique for that particular instance. It is then useful to track the correspondingly correct dtor along with the smart pointer, so that other code points that interact with that smart pointer do not then need to know about the allocation/deallocation policy at compile time. In this context, it is a form of information hiding and reducing code duplication (DRY).Exostosis
D
6

This works. The destruction happens properly.

class Base
{
    public:
     Base() { std::cout << "Base::Base\n"; }
     virtual ~Base() { std::cout << "Base::~Base\n"; }
};


class Derived : public Base
{
    public:
     Derived() { std::cout << "Derived::Derived\n"; }
     virtual ~Derived() { std::cout << "Derived::~Derived\n"; }
};

void Delete(const Base* bp)
{
    delete bp;
}

int main()
{
    std::unique_ptr<Base, void(*)(const Base*)> ptr = std::unique_ptr<Derived, void(*)(const Base*)>(new Derived(), Delete);
}
Detrital answered 25/11, 2011 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.