Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template Parameter that can accept function pointer or Functor

I'm trying to write an RAII-compliant resource wrapper, and I'm getting stuck on how to form the semantics of the template parameters.

For example, I could write a function to delete my resource:

void int_cleaner(int val) {
    std::cout << "Value of " << val << " has been cleaned up." << std::endl;
}

Or I could write it as a Functor:

struct int_deleter {
    void operator()(int val) const {
        std::cout << "Value of " << val << " has been cleaned up." << std::endl;
    }
};

But here's where I get stuck: if I want to pass this to my resource wrapper, I have to change how the template parameter is defined.

If I write resource like this:

template<typename T, typename Deleter>
class resource {
};

This works fine with a functor, but not with the function itself.

int main() {
    resource<int, int_deleter> res; //Compiles fine
    //resource<int, int_cleaner> res2; //Does Not Compile
    return 0;
}

Conversely, if I write the template parameters like this:

template<typename T>
using deleter_t = void(*)(T);

template<typename T, deleter_t<T> Deleter>
class resource {
};

int main() {
    //resource<int, int_deleter> res; //Does Not Compile
    resource<int, int_cleaner> res2; //Compiles fine
    return 0;
}

Now, I could write both versions of the code, but there's two reasons I don't want to do that:

  1. I'd just be duplicating the definition for resource, and if I need to make a change to one, I need to make changes to the other as well.
  2. The version that accepts function pointers won't accept a version like void cleaner(T const&), because that won't bind to void(*)(T). So I'd also need to make two or three more versions so that I can handle T, T&, T const&, and T&&.

How can I write the resource wrapper in such a way that minimizes code duplication, especially given that the deletion mechanism is going to vary between the functor version and the function pointer version?

//example:
template<typename T>
using deleter_t = void(*)(T);

template<typename T, deleter_t<T> Deleter>
class resource {
~resource() {Deleter(val);}
};
template<typename T, typename Deleter>
class resource {
~resource() {Deleter{}(val);}//Note the subtle syntax change
};
like image 566
Xirema Avatar asked Oct 02 '17 16:10

Xirema


People also ask

How do you pass a functor as a function?

You cannot pass a functor as a function pointer into a function that takes a function pointer, even if the functor has the same arguments and return value as the function pointer. Similarly, if a function expects a functor, you cannot pass in a function pointer.

What is template type parameter?

A template parameter is a special kind of parameter that can be used to pass a type as argument: just like regular function parameters can be used to pass values to a function, template parameters allow to pass also types to a function.

What is the difference between a functor and a function pointer?

A function pointer allows a pointer to a function to be passed as a parameter to another function. Function Objects (Functors) - C++ allows the function call operator() to be overloaded, such that an object instantiated from a class can be "called" like a function.

What are non type parameters for templates?

A template non-type parameter is a template parameter where the type of the parameter is predefined and is substituted for a constexpr value passed in as an argument. A non-type parameter can be any of the following types: An integral type. An enumeration type.


1 Answers

Do

template<typename T, typename Deleter>
class resource {
};

then write

template<auto k>
using constant_t = std::integral_constant<std::decay_t<decltype(k)>, k>;
template<auto k>
constexpr constant_t<k> constant{};

Now your main looks like this:

int main() {
  resource<int, int_deleter> res; //Compiles fine
  resource<int, constant_t<int_cleaner>> res2; //Also compiles fine
  return 0;
}

and we are done.

Live example.

This is c++17.

In c++14 you'd have to replace the constant_t<foo> with std::integral_constant<std::decay_t<decltype(foo)>, foo> because it lacks auto template parameters.

In c++11 integral_constant doesn't work with function pointers and let you call them. You'll have to write a derived type:

namespace notstd {
  template<class T, T t>
  struct integral_constant:std::integral_constant<T, t> {
    constexpr operator T()const{ return this->get(); }
  }
}

and replace std::integral_constant with notstd::integral_constant to enable that feature. (Implicit conversion to function pointer is sufficient to permit using the call operator on the integral constant).

In c++03 you'll want to get a new compiler.


Another approach in c++17 would be to go all value instead of all type.

resource foo(7, int_deleter{});
resource foo2(7, int_cleaner);

and teach resources to hold values to their deleter. This results in the int_cleaner taking storage and having a value in the resource.

resource foo(7, int_deleter{});
resource foo2(7, constant<int_cleaner>);

goes back to the original plan, where we make a stateless int_cleaner pointer by lifting it into the type system.

By using EBO, resource can store stateless deleters with zero overhead.


Note that your resource looks a lot like a unique_ptr<T, Deleter>, where Deleter::pointer is a thinly wrapped std::optional<T> (for nullability).

template<class T>
struct nullable_opt:std::optional<T> {
  using std::optional<T>::optional;
  nullable_opt( nullable_opt const& ) = default;
  nullable_opt( nullable_opt && ) = default;
  nullable_opt& operator=( nullable_opt const& ) = default;
  nullable_opt& operator=( nullable_opt && ) = default;
  nullable_opt() = default;

  nullable_opt(std::nullptr_t) {}
};

or somesuch.

like image 183
Yakk - Adam Nevraumont Avatar answered Sep 27 '22 18:09

Yakk - Adam Nevraumont