Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using automatic deduction with unique_ptr and custom deleter

I'm currently using a C library that defines a number of data types, all of which need to have their lifetimes managed by the user. There are a number of functions defined in this fashion:

int* create() {
    return new int();
}

void destroy(int* i) {
    delete i;
}

Most of these don't need to be accessed after creation. They simply need to exist. Because of this, I'm trying to manage them using unique_ptr's declared in the scope of which I need them to live.

This is how such a declaration looks like:

// Note that I'm avoiding writing the type's name manually.
auto a = std::unique_ptr<std::decay_t<decltype(*create())>, decltype(&destroy)>{create(), &destroy};

But this is overly verbose, so I encapsulated it in a utility function template:

template<typename T>
auto make_unique_ptr(T* p, void (*f)(T*)) {
    return std::unique_ptr<T, decltype(f)>(p, f);
}

Which is used this way:

auto b = make_unique_ptr(create(), &destroy);

This looks nice, but introduces a non-standard function that has no real purpose other than being syntactic sugar for some declarations. My colleagues may not even know it exists and end up creating other versions of it with different names.

c++17 Introduced class template argument deduction. I thought this is the perfect solution to my problem: A standard way to deduce all those types without resorting to user-defined wrappers. So I tried this:

auto c = std::unique_ptr{create(), &destroy};

As is the rule with C++ compilers and templates, this fails with a several lines long error message. Here are the relevant parts:

(...): error: class template argument deduction failed:
 auto c = std::unique_ptr{create(), &destroy};
                                            ^
(...): note: candidate: 'template<class _Tp, class _Dp> 
unique_ptr(std::unique_ptr<_Tp, _Dp>::pointer, typename std::remove_reference<_Dp>::type&&)-> std::unique_ptr<_Tp, _Dp>'
       unique_ptr(pointer __p,
       ^~~~~~~~~~
(...): note:   template argument deduction/substitution failed:
(...): note:   couldn't deduce template parameter '_Tp'
     auto c = std::unique_ptr{create(), &destroy};
                                                ^

Theoretically, I could add a deduction guide to handle this:

namespace std {

template<typename T>
unique_ptr(T* p, void (*f)(T*)) -> unique_ptr<T, decltype(f)>;

}

And it does work at least on my version of gcc, but the standard doesn't like it very much:

[namespace.std]

1 Unless otherwise specified, the behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std.

4 The behavior of a C++ program is undefined if it declares
    (...)
    4.4 - a deduction guide for any standard library class template.

There's also some problems related to differentiating between pointers and arrays, but let's ignore that.

Finally, the question(s): Is there any other way to 'help' std::unique_ptr (or maybe std::make_unique) to deduce the correct type when using custom deleters? Just in case this is an XY problem, is there any solutions I didn't think of for lifetime management of those types (perhaps std::shared_ptr)? If both of those answers are a negative, is there any improvements on c++20 I should be looking forward to, that solve this issue?

Feel free to test the above examples at Coliru.

like image 215
Cássio Renan Avatar asked Jul 09 '18 18:07

Cássio Renan


People also ask

When should we use unique_ptr?

Use unique_ptr when you want to have single ownership(Exclusive) of the resource. Only one unique_ptr can point to one resource. Since there can be one unique_ptr for single resource its not possible to copy one unique_ptr to another. A shared_ptr is a container for raw pointers.

What happens when unique_ptr goes out of scope?

unique_ptr. An​ unique_ptr has exclusive ownership of the object it points to and ​will destroy the object when the pointer goes out of scope.

Does unique_ptr call Delete?

unique_ptr objects automatically delete the object they manage (using a deleter) as soon as they themselves are destroyed, or as soon as their value changes either by an assignment operation or by an explicit call to unique_ptr::reset.

What happens when you move a unique_ptr?

A unique_ptr can only be moved. This means that the ownership of the memory resource is transferred to another unique_ptr and the original unique_ptr no longer owns it.


1 Answers

I would recommend writing custom deleters rather than using function pointers. Using the function pointer would make all the unique_ptrs twice the size for no reason.

Instead, write a deleter templated on a function pointer:

template <auto deleter_f>
struct Deleter {
    template <typename T>
    void operator()(T* ptr) const
    {
        deleter_f(ptr);
    }
};

Or, as Yakk - Adam Nevraumont mentioned in the comments:

template <auto deleter_f>
using Deleter = std::integral_constant<std::decay_t<decltype(deleter_f)>, deleter_f>;

Using it becomes pretty clean:

auto a = std::unique_ptr<int, Deleter<destroy>>{create()};

Although you might want to combine this with your make_unique_ptr function:

template <auto deleter_f, typename T>
auto create_unique_ptr(T* ptr)
{
    return std::unique_ptr<T, Deleter<deleter_f>>{ptr};
}

// Usage:
auto a = create_unique_ptr<destroy>(create());
like image 122
Justin Avatar answered Nov 14 '22 20:11

Justin