[smart_ptr] Is there any interest in unique_ptr with type erased deleter?
It is widely known that deleter type is part of std::unique_ptr type. It is the most effective decision, but sometimes not very convenient. The most important use cases (for me) when std::unique_ptr<T, std::default_delete<T>> doesn't work are following: 1. unique_ptr to incomplete class struct MyClass; unique_ptr<MyClass> create(); auto ptr = create(); // compilation error 2. upcast to base without polymorphic destructor struct Base { // ... // no virtual ~Base() here }; struct Derived : Base { // ... }; unique_ptr<Base> ptr = make_unique<Derived>(); // compiles by leaks! 3. unique_ptr with non-default deleter unique_ptr<FILE, int (FILE *)> open_file(std::string const & path) // works but looks ugly, why should the fact that fclose returns `int` be visible from the signature of the function `open_path`? { return { std::fopen(path, "r"), &std::fclose }; } Of course, all this examples could be fixed if unique_ptr would be replaced by shared_ptr, but the semantic of the shared ownership is not desirable often. I have implemented smart pointer with semantics of unique ownership and type erased deleter (https://github.com/AndreyG/unnamed). It has size of 3 pointers and it doesn't require additional memory allocations for the case when deleter is empty class (for example, std::default_delete or non-capturing lambda) and for the case when deleter is known at compile time function (for instance, fclose). The are some simple examples of usage and tests in the repo. Why don't just use unique_ptr<T, function<void (void *>>? The main reason is that due to the small object optimization std::function has rather big size. For instance, sizeof(unique_ptr<T, function<void (void *>>) == 72 for MSVC x64! Results for the other platforms can be found in the repo README. Does it seem useful? -- Andrey Davydov
2017-03-21 15:04 GMT+08:00 Andrey Davydov via Boost <boost@lists.boost.org>: [...]
I have implemented smart pointer with semantics of unique ownership and type erased deleter (https://github.com/AndreyG/unnamed). It has size of 3 pointers and it doesn't require additional memory allocations for the case when deleter is empty class (for example, std::default_delete or non-capturing lambda) and for the case when deleter is known at compile time function (for instance, fclose). The are some simple examples of usage and tests in the repo.
Why don't just use unique_ptr<T, function<void (void *>>? The main reason is that due to the small object optimization std::function has rather big size. For instance, sizeof(unique_ptr<T, function<void (void *>>) == 72 for MSVC x64! Results for the other platforms can be found in the repo README.
Does it seem useful?
Why not: ``` template<class T> using my_ptr = unique_ptr<T, void(*)(T*)>; ``` ? Even if you need a stateful deleter, you don't need to reinvent the whole unique_ptr, just use your custom type-erased deleter.
On Tue, Mar 21, 2017 at 11:10 AM, TONGARI J via Boost <boost@lists.boost.org
wrote:
2017-03-21 15:04 GMT+08:00 Andrey Davydov via Boost <boost@lists.boost.org
: [...]
I have implemented smart pointer with semantics of unique ownership and type erased deleter (https://github.com/AndreyG/unnamed). It has size of 3 pointers and it doesn't require additional memory allocations for the case when deleter is empty class (for example, std::default_delete or non-capturing lambda) and for the case when deleter is known at compile time function (for instance, fclose). The are some simple examples of usage and tests in the repo.
Why don't just use unique_ptr<T, function<void (void *>>? The main reason is that due to the small object optimization std::function has rather big size. For instance, sizeof(unique_ptr<T, function<void (void *>>) == 72 for MSVC x64! Results for the other platforms can be found in the repo README.
Does it seem useful?
Why not: ``` template<class T> using my_ptr = unique_ptr<T, void(*)(T*)>; ``` ? Even if you need a stateful deleter, you don't need to reinvent the whole unique_ptr, just use your custom type-erased deleter.
Because I'd like, for example, cast my_ptr<Derived> to my_ptr<Base>. -- Andrey Davydov
It’s an interesting and, on the face of it, obviously useful idea. What is required, more than simply creating a type-erased deleter? Isn’t this simply a partial specialisation of std::unique_ptr?
On 21 Mar 2017, at 08:04, Andrey Davydov via Boost <boost@lists.boost.org> wrote:
It is widely known that deleter type is part of std::unique_ptr type. It is the most effective decision, but sometimes not very convenient. The most important use cases (for me) when std::unique_ptr<T, std::default_delete<T>> doesn't work are following: 1. unique_ptr to incomplete class struct MyClass; unique_ptr<MyClass> create(); auto ptr = create(); // compilation error
2. upcast to base without polymorphic destructor struct Base { // ... // no virtual ~Base() here }; struct Derived : Base { // ... }; unique_ptr<Base> ptr = make_unique<Derived>(); // compiles by leaks!
3. unique_ptr with non-default deleter unique_ptr<FILE, int (FILE *)> open_file(std::string const & path) // works but looks ugly, why should the fact that fclose returns `int` be visible from the signature of the function `open_path`? { return { std::fopen(path, "r"), &std::fclose }; }
Of course, all this examples could be fixed if unique_ptr would be replaced by shared_ptr, but the semantic of the shared ownership is not desirable often. I have implemented smart pointer with semantics of unique ownership and type erased deleter (https://github.com/AndreyG/unnamed). It has size of 3 pointers and it doesn't require additional memory allocations for the case when deleter is empty class (for example, std::default_delete or non-capturing lambda) and for the case when deleter is known at compile time function (for instance, fclose). The are some simple examples of usage and tests in the repo.
Why don't just use unique_ptr<T, function<void (void *>>? The main reason is that due to the small object optimization std::function has rather big size. For instance, sizeof(unique_ptr<T, function<void (void *>>) == 72 for MSVC x64! Results for the other platforms can be found in the repo README.
Does it seem useful?
-- Andrey Davydov
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
On Tue, Mar 21, 2017 at 12:00 PM, Richard Hodges <hodges.r@gmail.com> wrote:
What is required, more than simply creating a type-erased deleter?
Also deleter must capture pointer, because pointer which was passed to constructor can differ from the pointer which will be deleted. For example, struct Base { /* ... */ }; struct Derived : Base { /* ... */ }; ptr<Derived> p1(new Derived); void * raw_p1 = p1.get(); ptr<Base> p2 = std::move(p1); void * raw_p2 = p2.get(); assert(raw_p1 != raw_p2);
Isn’t this simply a partial specialisation of std::unique_ptr?
It seems to me that to declare specialization of std::unique_ptr is undefined behavior. -- Andrey Davydov.
On 03/21/17 12:54, Andrey Davydov via Boost wrote:
On Tue, Mar 21, 2017 at 12:00 PM, Richard Hodges <hodges.r@gmail.com> wrote:
What is required, more than simply creating a type-erased deleter?
Also deleter must capture pointer, because pointer which was passed to constructor can differ from the pointer which will be deleted. For example,
struct Base { /* ... */ }; struct Derived : Base { /* ... */ };
ptr<Derived> p1(new Derived); void * raw_p1 = p1.get(); ptr<Base> p2 = std::move(p1); void * raw_p2 = p2.get(); assert(raw_p1 != raw_p2);
The above piece of code strikes me as very unsafe and better be avoided in the first place. Designing new tools to handle this case is IMHO misguided. Now, I can understand the need to perform casts on smart-pointers, but they should still provide some level of safety, at least the same level that is provided by pointer casts in the core language. But supporting casts should not require a new smart-pointer. And indeed it doesn't: template< typename U, typename T, typename D > std::unique_ptr< U, D > static_pointer_cast( std::unique_ptr< T, D >&& p) { D d = p.get_deleter(); return std::unique_ptr< U, D >( static_cast< U* >(p.release()), std::move(d)); } The other part of your proposal, which is polymorphic behavior with a deleter knowing the type to call the destructor on, can be solved by a custom deleter. I'm not sure a generalized version of such deleter would have a large demand, given that there is the alternative TONGARI J suggested, but I'm not opposed to a proposal.
Here's a first cut of a polymorphic deleter (which at least allows you to spell the name of the unique_ptr<A> in the example). It's not optimally efficient, but a little SFO should do it. #include <memory> #include <iostream> namespace notstd { class polymorphic_deleter { struct concept { virtual void impl_call() const = 0; }; template<class T, class Deleter> struct model final : concept { model(T *p, Deleter del) : p_(p) , del_(std::move(del)) {} void impl_call() const { del_(p_); } T *p_; Deleter del_; }; std::unique_ptr<concept> impl_; template<class T, class Del> static auto make_model(T* p, Del&& del) { using deleter_type = std::decay_t<Del>; using pointer_type = T; return std::make_unique<model<pointer_type, deleter_type>> ( p, std::forward<Del>(del) ); }; public: template<class T, class Del> polymorphic_deleter(T* p, Del&& del) : impl_(make_model(p, std::forward<Del>(del))) {} void operator()(void*) const { impl_->impl_call(); } }; template<class T, class...Args> auto make_polymorphic_unique(Args&&...args) { auto pt = std::make_unique<T>(std::forward<Args>(args)...); auto del = polymorphic_deleter(pt.get(), std::default_delete<T>()); return std::unique_ptr<T, polymorphic_deleter>(pt.release(), std::move(del)); }; } struct A { ~A() { std::cout << "deleting A\n"; } }; struct B : A { ~B() { std::cout << "deleting B\n"; } }; int main() { auto pb = notstd::make_polymorphic_unique<B>(); auto pa = std::unique_ptr<A, notstd::polymorphic_deleter>(std::move(pb)); pa.reset(); } On 21 March 2017 at 12:15, Andrey Semashev via Boost <boost@lists.boost.org> wrote:
On 03/21/17 12:54, Andrey Davydov via Boost wrote:
On Tue, Mar 21, 2017 at 12:00 PM, Richard Hodges <hodges.r@gmail.com> wrote:
What is required, more than simply creating a type-erased deleter?
Also deleter must capture pointer, because pointer which was passed to constructor can differ from the pointer which will be deleted. For example,
struct Base { /* ... */ }; struct Derived : Base { /* ... */ };
ptr<Derived> p1(new Derived); void * raw_p1 = p1.get(); ptr<Base> p2 = std::move(p1); void * raw_p2 = p2.get(); assert(raw_p1 != raw_p2);
The above piece of code strikes me as very unsafe and better be avoided in the first place. Designing new tools to handle this case is IMHO misguided.
Now, I can understand the need to perform casts on smart-pointers, but they should still provide some level of safety, at least the same level that is provided by pointer casts in the core language. But supporting casts should not require a new smart-pointer. And indeed it doesn't:
template< typename U, typename T, typename D > std::unique_ptr< U, D > static_pointer_cast( std::unique_ptr< T, D >&& p) { D d = p.get_deleter(); return std::unique_ptr< U, D >( static_cast< U* >(p.release()), std::move(d)); }
The other part of your proposal, which is polymorphic behavior with a deleter knowing the type to call the destructor on, can be solved by a custom deleter. I'm not sure a generalized version of such deleter would have a large demand, given that there is the alternative TONGARI J suggested, but I'm not opposed to a proposal.
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman /listinfo.cgi/boost
On Tue, Mar 21, 2017 at 2:22 PM, Richard Hodges via Boost < boost@lists.boost.org> wrote:
Here's a first cut of a polymorphic deleter (which at least allows you to spell the name of the unique_ptr<A> in the example). It's not optimally efficient, but a little SFO should do it.
#include <memory> #include <iostream>
namespace notstd {
class polymorphic_deleter { struct concept { virtual void impl_call() const = 0; };
template<class T, class Deleter> struct model final : concept { model(T *p, Deleter del) : p_(p) , del_(std::move(del)) {}
void impl_call() const { del_(p_); }
T *p_; Deleter del_; };
std::unique_ptr<concept> impl_;
template<class T, class Del> static auto make_model(T* p, Del&& del) { using deleter_type = std::decay_t<Del>; using pointer_type = T; return std::make_unique<model<pointer_type, deleter_type>> ( p, std::forward<Del>(del) ); };
public:
template<class T, class Del> polymorphic_deleter(T* p, Del&& del) : impl_(make_model(p, std::forward<Del>(del))) {}
void operator()(void*) const { impl_->impl_call(); }
};
template<class T, class...Args> auto make_polymorphic_unique(Args&&...args) { auto pt = std::make_unique<T>(std::forward<Args>(args)...); auto del = polymorphic_deleter(pt.get(), std::default_delete<T>()); return std::unique_ptr<T, polymorphic_deleter>(pt.release(), std::move(del)); };
}
struct A { ~A() { std::cout << "deleting A\n"; } };
struct B : A { ~B() { std::cout << "deleting B\n"; } };
int main() { auto pb = notstd::make_polymorphic_unique<B>(); auto pa = std::unique_ptr<A, notstd::polymorphic_deleter>( std::move(pb)); pa.reset(); }
Class `polymorphic_deleter` is implementation of the `function<void (void *)>` without small object optimization. Yes, it is possible to use unique_ptr with such deleter, but it requires additional memory allocation even for the case when deleter is std::default_delete. I have tried to avoid this. -- Andrey Davydov
Richard Hodges wrote:
Here's a first cut of a polymorphic deleter...
It's easier to write if you wrap boost::detail::shared_count. That way you'd even be able to form weak pointers (although I'm not sure we have the necessary public interface for that.) shared_ptr<void> works as a deleter too, this keeps us entirely within the public interface, but we'll waste a pointer. Although with it you'll be able to use make_shared inside make_poly_unique.
Richard Hodges wrote:
Here's a first cut of a polymorphic deleter...
It's easier to write if you wrap boost::detail::shared_count. That way you'd even be able to form weak pointers (although I'm not sure we have the necessary public interface for that.)
shared_ptr<void> works as a deleter too, this keeps us entirely within the public interface, but we'll waste a pointer. Although with it you'll be able to use make_shared inside make_poly_unique.
Although on second thought both of these break horribly after release().
On Tue, Mar 21, 2017 at 2:15 PM, Andrey Semashev via Boost < boost@lists.boost.org> wrote:
On 03/21/17 12:54, Andrey Davydov via Boost wrote:
On Tue, Mar 21, 2017 at 12:00 PM, Richard Hodges <hodges.r@gmail.com> wrote:
What is required, more than simply creating a type-erased deleter?
Also deleter must capture pointer, because pointer which was passed to constructor can differ from the pointer which will be deleted. For example,
struct Base { /* ... */ }; struct Derived : Base { /* ... */ };
ptr<Derived> p1(new Derived); void * raw_p1 = p1.get(); ptr<Base> p2 = std::move(p1); void * raw_p2 = p2.get(); assert(raw_p1 != raw_p2);
The above piece of code strikes me as very unsafe and better be avoided in the first place. Designing new tools to handle this case is IMHO misguided.
Why this code is very unsafe? If change `ptr` to `shared_ptr` this will become perfectly valid and widely used pattern.
Now, I can understand the need to perform casts on smart-pointers, but they should still provide some level of safety, at least the same level that is provided by pointer casts in the core language. But supporting casts should not require a new smart-pointer. And indeed it doesn't:
template< typename U, typename T, typename D > std::unique_ptr< U, D > static_pointer_cast( std::unique_ptr< T, D >&& p) { D d = p.get_deleter(); return std::unique_ptr< U, D >( static_cast< U* >(p.release()), std::move(d)); }
What is `D`? If it is std::default_delete<T> then function result type will be std::unique_ptr<U, std::default_delete<T>> and unlikely this type is very useful (even if it compiles).
The other part of your proposal, which is polymorphic behavior with a deleter knowing the type to call the destructor on, can be solved by a custom deleter. I'm not sure a generalized version of such deleter would have a large demand, given that there is the alternative TONGARI J suggested, but I'm not opposed to a proposal.
May be I doesn't understand what TONGARI J suggested, but how can `unique_ptr<T, void(*)(T*)>` hold stateful deleter? -- Andrey Davydov
May be the best solution will be implement type_erased_unique_ptr
Re-posting here as I replied direct to Andrey by mistake: publicly inheriting from std::unique_ptr<T, type_erased_deleter> and defining own set of constructors. What do you think about this? I think that's reasonable. The conversion constructor can take care of marshalling the pointer and deleter into the polymorphic deleter. Again though, is this necessary? Won't a partial specialisation take care of this? The c++ standard mandates that specialisations of std:: template classes must provide the services specified at a *minimum*. It does not prevent them from offering more services (such as a conversion constructor). By which I mean, this is legal, AFAIK: namespace std { template<class T> struct unique_ptr<T, ::notstd::polymorphic_deleter> { template<class U, class D, std::enable_if_t<not std::is_same<D, ::notstd::polymorphic_deleter>::value>* = nullptr> unique_ptr(std::unique_ptr<U, D>&& other) { // ... } // etc... implement the rest of unique_ptr's interface }; } On 21 March 2017 at 12:54, Andrey Davydov via Boost <boost@lists.boost.org> wrote:
On Tue, Mar 21, 2017 at 2:15 PM, Andrey Semashev via Boost < boost@lists.boost.org> wrote:
On 03/21/17 12:54, Andrey Davydov via Boost wrote:
On Tue, Mar 21, 2017 at 12:00 PM, Richard Hodges <hodges.r@gmail.com> wrote:
What is required, more than simply creating a type-erased deleter?
Also deleter must capture pointer, because pointer which was passed to constructor can differ from the pointer which will be deleted. For example,
struct Base { /* ... */ }; struct Derived : Base { /* ... */ };
ptr<Derived> p1(new Derived); void * raw_p1 = p1.get(); ptr<Base> p2 = std::move(p1); void * raw_p2 = p2.get(); assert(raw_p1 != raw_p2);
The above piece of code strikes me as very unsafe and better be avoided in the first place. Designing new tools to handle this case is IMHO misguided.
Why this code is very unsafe? If change `ptr` to `shared_ptr` this will become perfectly valid and widely used pattern.
Now, I can understand the need to perform casts on smart-pointers, but they should still provide some level of safety, at least the same level that is provided by pointer casts in the core language. But supporting casts should not require a new smart-pointer. And indeed it doesn't:
template< typename U, typename T, typename D > std::unique_ptr< U, D > static_pointer_cast( std::unique_ptr< T, D >&& p) { D d = p.get_deleter(); return std::unique_ptr< U, D >( static_cast< U* >(p.release()), std::move(d)); }
What is `D`? If it is std::default_delete<T> then function result type will be std::unique_ptr<U, std::default_delete<T>> and unlikely this type is very useful (even if it compiles).
The other part of your proposal, which is polymorphic behavior with a deleter knowing the type to call the destructor on, can be solved by a custom deleter. I'm not sure a generalized version of such deleter would have a large demand, given that there is the alternative TONGARI J suggested, but I'm not opposed to a proposal.
May be I doesn't understand what TONGARI J suggested, but how can `unique_ptr<T, void(*)(T*)>` hold stateful deleter?
-- Andrey Davydov
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/ mailman/listinfo.cgi/boost
participants (5)
-
Andrey Davydov
-
Andrey Semashev
-
Peter Dimov
-
Richard Hodges
-
TONGARI J