Template Parameter that can accept function pointer or Functor
Asked Answered
F

2

7

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
};
Fool answered 2/10, 2017 at 16:59 Comment(3)
std::unique_ptr et al. have the same problem. They support passing the deleter object by value, so that you can pass the appropriate function pointer. Don't know if this is the best solution though.Unicef
Try resource<int, int(&)(int)> to use int_cleaner as a deleter.Pronouncement
A solution could be to create a template <auto FUNCTION> struct wrapper, and pass this as template parameter to resource: resource<int, wrapper<int_cleaner>>. You have to make some compromise I think, because int_cleaner is function, but int_deleter is a type (so they cannot be mapped to the same template parameter at the same time).Nikos
P
6

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 .

In 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 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 you'll want to get a new compiler.


Another approach in 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.

Pucker answered 2/10, 2017 at 17:20 Comment(3)
I do have a c++17 compiler, so that's not an issue. I like this solution, though I'm hoping there's a version that prevents me from having to add the (what seems to me like boilerplate) use of constant_t in local type declarations. If there isn't though, this is probably clean enough as is for me to reliably use it.Fool
@Fool The same template parameter cannot be both a type and a value. You could also use deduction guides and value-based types.Pucker
@Fool Oh yes, and you can do template<typename T, deleter_t<T> Deleter> using resource_fptrdeleter = resource<T, constant<Deleter>>; (pick a better name) to not DRY; convert the compile time function pointer into a type in one spot.Pucker
J
4

Your template arguments are types. To get a type from a "value" you can use decltype. Like e.g.

resource<int, decltype(int_cleaner)> res2;

You still need to pass the actual function as an argument to some function (like the constructor) though, as you can't create an instance from a function type.

I recommend you take a look at standard classes that do the same thing, like e.g. std::unique_ptr. Which maybe you should be using instead of creating your own class? Or perhaps std::shared_ptr?

Jokester answered 2/10, 2017 at 17:4 Comment(5)
std::unique_ptr and std::shared_ptr intend to have their objects Heap-Allocated, which isn't appropriate for the code I'm trying to write.Fool
@Fool Not exactly. They only intend for their objects to be heap-allocated if that's where the pointer came from. You can define custom deleters on them, and I strongly recommend you do rather than writing your own class. For example, you can wrap a C api that requires releasing a resource (even if it's not a pointer IIRC) using std::unique_ptr and a custom deleterJansen
@Jansen yes, and I've done that before. But the problem is that they misbehave the moment they go out of scope. You end up having to create an object which both maintains the lifetime of the object and the unique_ptr, which then leads back to my original problem of how you write the wrapper, only with extra details. If I weren't on mobile, I'd show you a motivating example.Fool
@Fool You should not see the smart pointers a simply auto-deleting pointers, but more from an ownership perspective. Trough them you can have a single or shared ownership of a resource, which thanks to RAII will be automatically released when needed.Jokester
I know how to think about unique_ptr and shared_ptr. You need to take it as a given that, having considered those options, I still determined that they were inappropriate for my specific problem.Fool

© 2022 - 2024 — McMap. All rights reserved.