0

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.

puio
  • 1,208
  • 6
  • 19
  • 1
    A Stack Overflow question should clearly describe the issue, such as a program that you expect to compile but does not. – Brian Bi Nov 09 '20 at 21:30
  • `template`? That doesn't seem to have anything to do with `std::unique_ptr` though, that's a basic template topic – UnholySheep Nov 09 '20 at 21:33
  • 1
    It seems that your non-member cleanup function should always have type `void (*)(T*)`... no template type parameter `typename FreeFunc` is needed. – Ben Voigt Nov 09 '20 at 22:22
  • @BenVoigt Thanks for the suggestion, but that adds the possibility of adding throwing statements via the function pointer, 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 unique_ptr. – puio Nov 10 '20 at 06:40
  • So, is your question "How do I make a class template that wraps a function in a noexcept callable object, to use as a `std::unique_ptr` custom deleter?" – parktomatomi Nov 10 '20 at 10:58
  • @parktomatomi it seems so – puio Nov 10 '20 at 10:59
  • @puio: I didn't say to eliminate the `ObjectDeleter` wrapper, which is where you are able to annotate `noexcept` (but you forgot to do so). None of your existing code does anything to prevent exceptions. – Ben Voigt Nov 10 '20 at 15:09
  • @BenVoigt passing throwing statements via function pointer will go undetected by compiler/ static analyzers. Using lambda as parameter avoids that issue. – puio Nov 10 '20 at 15:11

1 Answers1

1

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 }
};
parktomatomi
  • 3,851
  • 1
  • 14
  • 18
  • "lambdas can't be used as template arguments" how does the following work ? https://godbolt.org/z/zcfaM7 – puio Nov 11 '20 at 07:38
  • You're passing the lambda as a function argument there, which has been fine since C++11. You can't pass it as a non-type template argument untill C++20. You also can't put a lambda in `decltype` and pass it as a type argument until C++20. https://godbolt.org/z/sE9aP3 – parktomatomi Nov 11 '20 at 07:49
  • okay, thanks! https://godbolt.org/z/cv9d3M It's crashing. you need to run the output in the demo link you gave. – puio Nov 11 '20 at 15:52
  • Yikes, thanks for correct me! It was calling the default constructor of the function pointer type, which of course is just a null pointer. Fixed answer. – parktomatomi Nov 12 '20 at 08:32