Search code examples
c++templatesunique-ptrparameterizationgeneric-lambda

How to make a class template that wraps a function in a noexcept-detectable, callable object, to use as a std::unique_ptr custom deleter?


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.


Solution

  • 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 }
    };