10

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.

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 I should be looking forward to, that solve this issue?

Feel free to test the above examples at Coliru.

Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55
  • 2
    FWIW, unless you need to change the deleters at runtime, don't use a function pointer deleter like this. Without it, with an explicit type, `sizeof(unique_ptr) == sizeof(T*)`. With it, `sizeof(unique_ptr) == sizeof(T*) + sizeof(deleter)`, which is twice the size in this case – Justin Jul 09 '18 at 18:48
  • 2
    Why not a `make_library_type_nnn()` that returns the right `unique_ptr` and calls the right `create` function? – NathanOliver Jul 09 '18 at 18:49
  • @NathanOliver IMHO, it doesn't scale very well. There are tons (Over 100) of those types, and most have multiple ways to be created. – Not a real meerkat Jul 09 '18 at 18:53
  • @Justin Thanks for the info. It maybe doesn't matter that much (The actual structures are a bit more complex than a single `int`). I may have to measure the difference to be sure, but that's a lot of work that I'm kind of trying to avoid right now. – Not a real meerkat Jul 09 '18 at 18:57
  • @CássioRenan It does matter. The structures might be complex, but suddenly your `unique_ptr`s are twice the size. If you have a `std::vector>`, suddenly half of the memory is wasted in that vector. It's not too hard to write a custom deleter, and you can even write one which takes the function pointer to the deleter as a template argument – Justin Jul 09 '18 at 18:58
  • @Justin Like Cássio Renan said, it might not matter. A solution that saves memory is favorable to one that doesn't if all other things are equal. But all things are not equal if you include the work necessary to convert from one to the other. Developer time is a finite resource just like memory (and much more expensive) and you're exchanging one for the other. You can't assume the trade-off always favors computer memory. – François Andrieux Jul 09 '18 at 19:03
  • @FrançoisAndrieux Yes, but my point is that developer time is minimal here. It's remarkably easy to write a custom deleter and use it all over the place. Of course, if you have to write it for each deleter function, that's not worth it, but you can generalize it and write it once. – Justin Jul 09 '18 at 19:04
  • 3
    `templateusing deleter_t = std::integral_constant< std::decay_t, f >; template constexpr deleter_t deleter{}`; now `unique_ptr>` is the cost-free deleter you want. – Yakk - Adam Nevraumont Jul 09 '18 at 19:06
  • @Justin I see your point, but François basically stated my concern better than I could have worded myself. I would need to write a custom deleter for each of the types in the library. I'm trying to avoid that. – Not a real meerkat Jul 09 '18 at 19:07

4 Answers4

3

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());
Justin
  • 24,288
  • 12
  • 92
  • 142
  • _"Since the deleter is encoded in the type of the unique_ptr, your colleagues can't forget about it."_ won't they just `auto a = make_unqiue_ptr_by_colleague(create(), destroy);`? – Passer By Jul 09 '18 at 19:19
  • @PasserBy Sure, but then their `a` would by incompatible with the functions written by the OP, which would hopefully prompt the colleague to investigate what's going on – Justin Jul 09 '18 at 19:21
  • i don't think I got that. Do you mean when passing off to another function? OP said these may exist only for RAII. – Passer By Jul 09 '18 at 19:22
  • @PasserBy Ehh, good point. That point I made doesn't really do anything – Justin Jul 09 '18 at 19:23
  • I'm aware this (`make_unique_ptr_by_colleague`) is a very hard problem to solve. Yakk's answer helps a lot with that, but as I suspect I may not be able to implement it, I'll probably end up doing this instead. Thanks a lot! – Not a real meerkat Jul 09 '18 at 20:09
2

A type as a value:

template<class T>
struct tag_t {};
template<class T>
constexpr tag_t<T> tag{};

A value as a type:

template<auto f>
using val_t = std::integral_constant<std::decay_t<decltype(f)>, f>;
template<auto f>
constexpr val_t<f> val{};

Note that val<some_function> is an empty type that can be invoked in a constexpr context with () and it will call some_function. It can also store ints or whatever, but we are going to use it to store function pointers statelessly.

Now let's have fun:

namespace factory {
  // This is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to allocate an object of type T.
  template<class T>
  constexpr auto creator( tag_t<T> ) {
    return [](auto&&...args){
      return new T{ decltype(args)(args)... };
    };
  }
  // this is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to destroy that type
  template<class T>
  constexpr auto destroyer( tag_t<T> ) {
    return std::default_delete<T>{};
  }

  // This is a replacement for `std::unique_ptr`
  // that automatically finds the destroying function
  // object using ADL-lookup of `destroyer(tag<T>)`.
  template<class T>
  using unique_ptr = std::unique_ptr< T, decltype(destroyer(tag<T>)) >; // ADL magic here

  // This is a replacement for std::make_unique
  // that uses `creator` and `destroyer` to find
  // function objects to allocate and clean up
  // instances of T.
  template<class T, class...Args>
  unique_ptr<T> make_unique(Args&&...args) {
    // ADL magic here:
    return unique_ptr<T>( creator( tag<T> )(std::forward<Args>(args)...) );
  }
}

ok, that is a framework.

Now let's imagine you have some library. It has a type in it. It needs super secret special sauce to create and destroy instances of it here:

namespace some_ns {
  struct some_type {
    int x;
  };
  some_type* create( int a, int b ) {
    return new some_type{ a+b }; // ooo secret
  }
  void destroy( some_type* foo ) {
    delete foo; // ooo special
  }
}

and we want to connect it up. You reopen the namespace:

namespace some_ns {
  constexpr auto creator( tag_t<some_type> ) {
    return val<create>;
  }
  constexpr auto destoyer( tag_t<some_type> ) {
    return val<destroy>;
  }
}

and we are done.

factory::unique_ptr<some_ns::some_type> is the right type to store a unique ptr to some_type. To create it you just factory::make_unique<some_ns::some_type>( 7, 2 ) and you get a unique ptr of the correct type with a destroyer lined up, with zero memory overhead, and the call to the destroyer function involving no indirection.

Basically sweep std::unique_ptr and std::make_unique for factory::unique_ptr and factory::make_unique, then line up the creator/destroyer ADL helpers to make all unique ptrs to a given type just do the right thing.

Test code:

auto ptr = factory::make_unique<some_ns::some_type>( 2, 3 );
std::cout << ptr->x << "=5\n";
std::cout << sizeof(ptr) << "=" << sizeof(std::unique_ptr<some_ns::some_type>) << "\n";

live example.

This has zero runtime overhead over writing custom unique ptr types. If you don't overload creator or destroyer for a tag_t<X> in X's namespace (or in namespace factory), factory::make_unique returns a bog standard std::unique_ptr<X>. If you do, it injects compile-time information how to destroy it.

By default factory::make_unique<X> uses {} initialization so it works with aggregates.

The tag_t system is to enable ADL-based customization of factory::make_unique and factory::unique_ptr. It is useful elsewhere too. End users don't have to know about it, they just need to know you use factory::unique_ptr and factory::make_unique always.

Searching for std::make_unique and std::unique_ptr should find you cases where someone is violating this rule. Eventually (you hope) they'll notice all unique pointers are factory::unique_ptr instead of std.

The magic to add a type to the system is short enough and easy enough to copy that people can do it without having to understand ADL. I'd include a paragraph marked "you don't need to know this, but this is how this works" in comments somewhere.

This is possibly overdone; I was just thinking how I'd handle distributed traits and destruction/creation in with some of the new features. I haven't tried this in production, so there may be unrevealed problems.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This... May actually be doable. Oh, the magic of namespaces: Replacing some `using std::unique_ptr` with `using factory::unique_ptr` (And deprecating `make_unique_ptr`, which I would already do anyway) is much simpler than trying to find each single use. It's still a lot of work (defining all those tagged creators and destroyers) and it does have some problems (forgetting to create a tagged destroyer when you should will cause UB. Oops.), so I'll do my due dilligence. – Not a real meerkat Jul 09 '18 at 20:02
  • @cassio it is easyish to make having a creator but no destroyer be a hard error. The default creator just has to return a non-lambda; then static assert in the default destroyer that the creator is a default one. – Yakk - Adam Nevraumont Jul 09 '18 at 21:20
2

You are overcomplicating things. Simply specialize std::default_delete for your custom types, and you can use a vanilla std::unique_ptr.

A shame that std::shared_ptr doesn't use the same customization-point, there you will have to supply the deleter explicitly.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
  • What a coincidence. I was actually looking at the linked answer just now, since it popped up on the main page. Good stuff! I actually completely forgot about being able to specialize `::std` classes for custom types (Or even about `std::default_delete`, for that matter). Guess I have a lot to learn, still. – Not a real meerkat Jul 09 '18 at 20:36
  • Having said that, I'm probably not implementing this, unfortunately, as it suffers from the same problems I'm having with Yakk's answer. It doesn't solve the type deduction problem (To be fair, all of the other answers also focused more on the deleters than on the deduction, which is fine by me), and forgetting to specialize `std::default_delete` when I should will cause UB. I **really** don't want to write over 100 boilerplate specializations just to prettify some RAII types. – Not a real meerkat Jul 09 '18 at 22:11
  • You know, the preprocessor is *excellent* to write those specializations for you. You just give it the type and destroy-function. If you are feeling fancy and want to also specialize make_unique and make_shared, add the create-function too. That's writing it out once for the macro, one line for each type invoking the macro, and one line undefining the macro. – Deduplicator Jul 09 '18 at 22:15
  • Also, a program calling [`std::default_delete::operator()`](https://en.cppreference.com/w/cpp/memory/default_delete) with an incomplete type is ill-formed. So, if the library-implementer ran with that, [the compiler will complain](http://coliru.stacked-crooked.com/a/ddac2dbcea136791) if the specializations aren't provided. – Deduplicator Jul 09 '18 at 22:22
  • Yes, using the CPP would be my approach, but writing those lines isn't really the problem. There isn't exactly a list of types that I could use: I would have to dig through the documentation and some headers to find them. I would probably miss some, and would have to keep mantaining this list in case new types are introduced. About your other comment: I didn't get it: by the time I use them, all types are already complete. Or did you mean something else? – Not a real meerkat Jul 10 '18 at 14:02
1

One thing you could do is use a using statement to introduce an alias for std::unique_ptr<T, void (*)(T*)> like

template<typename T>
using uptr = std::unique_ptr<T, void (*)(T*)>;

Then you could use it like

auto u = uptr<int>{create(), &destroy};
NathanOliver
  • 171,901
  • 28
  • 288
  • 402