Is it possible to use object type and free functions as parameters for creating custom deleters for std::unique_ptr ?

I'm new to templates and came up till here:

#include <memory>

template<typename T, typename FreeFunc> struct ObjectDeleter {
  const FreeFunc &free_func;
  ObjectDeleter(const FreeFunc &free_func) : free_func(free_func){
      
  }
  void operator()(T *item) 
  {
    if (item) {
      free_func(item);
    }
  }
};
struct Foo{};
void internal_foo_free(Foo *){}
struct Bar{};
void internal_bar_free(Bar *){}

using unique_foo_ptr = 
         std::unique_ptr<Foo, ObjectDeleter<Foo>([](Foo *foo){internal_foo_free(foo);});
int main(){
    return 0;
}

error:

<source>:19:48: error: wrong number of template arguments (1, should be 2)
   19 |          std::unique_ptr<Foo, ObjectDeleter<Foo>([](Foo *foo){internal_foo_free(foo);});
      |                                                ^
<source>:3:48: note: provided for 'template<class T, class FreeFunc> struct ObjectDeleter'
    3 | template<typename T, typename FreeFunc> struct ObjectDeleter {
      |                                                ^~~~~~~~~~~~~
<source>:19:50: error: lambda-expression in template-argument only available with '-std=c++2a' or '-std=gnu++2a'
   19 |          std::unique_ptr<Foo, ObjectDeleter<Foo>([](Foo *foo){internal_foo_free(foo);});
      |                                                  ^
<source>:19:87: error: template argument 2 is invalid
   19 |          std::unique_ptr<Foo, ObjectDeleter<Foo>([](Foo *foo){internal_foo_free(foo);});
      |                                                                                       ^
Compiler returned: 1

I was suggested to use function pointer (and I extend it to std::function also):

but that adds the possibility of adding throwing statements via the function pointer (or std::function), which the compiler or static analysers won't be able to detect. Parameterising using lambda will make sure that the no-one can add throwing statements in the destructor of std::unique_ptr. This is what I mean by "noexcept-detectable, callable object"

I'm using C++17.

1

There are 1 best solutions below

4
parktomatomi On BEST ANSWER

It's complaining because lambdas can't be used as template arguments (before C++20 anyway). Otherwise, lambdas are already callable objects that will not throw unless the function body throws, no wrapper class needed. You just have to do this awkwardness:

auto myDeleter = [&x](Foo* v) { internal_foo_free(v); };
std::unique_ptr<Foo, decltype(myDeleter)> guard { create_foo(), myDeleter };

Originally, I interpreted this question as "I want compilation to fail if someone uses a custom deleter not marked noexcept". I asked about it, which I think is why you edited your title to include that wording.

noexcept is a qualifier for optimization hint/documentation purposes. It is totally on the honor system. You can throw right inside them, and your source will still compile (though a static analyzer might complain). If you wanted to enforce that a custom deleter only calls noexcept functions, in C++17 you can use a static assertion with the noexcept operator, which returns false if an expression calls a non-noexcept function:

template <auto F>
struct NoExceptDeleter{
    template <typename T>
    void operator ()(T* arg) const noexcept {
        static_assert(noexcept(F(arg)), "deleter must be marked noexcept");
        F(arg);
    }
};

void good_delete(foo* v) noexcept { free(v); }
std::unique_ptr<foo, NoExceptDeleter<good_delete>> good_guard { make_foo() };

void bad_delete(foo* v) { throw 0; }
std::unique_ptr<foo, NoExceptDeleter<bad_delete>> bad_guard { make_foo() }; // compile-error

Because this takes a function pointer, in C++17 you can only use it with non-capturing lambdas decayed to a function pointer with the + operator:

auto good_lambda = [](foo* v) noexcept { free(v); }
std::unique_ptr<foo, NoExceptDeleter<+good_lambda>> lambda_guard;

Demo: https://godbolt.org/z/vdEov3

If you needed to capture inside your lambda, you'd have to use a stateful deleter:

template <typename F>
struct StatefulNoExceptDeleter {
    F f;
    StatefulNoExceptDeleter(F f) : f(f) { }

    template <typename T>
    void operator ()(T* arg) const noexcept {
        static_assert(noexcept(f(arg)), "deleter must be marked noexcept");
        f(arg);
    }
};

/* ... */

int baz;
auto deleter = [&](Foo* t) noexcept {
    std::cout << "labmda: " << baz << std::endl;
    delete t;
};
std::unique_ptr<Foo, StatefulNoExceptDeleter<decltype(deleter)>> guard { 
    new Foo, 
    StatefulNoExceptDeleter<decltype(deleter)>{ deleter }
};