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:
resource
, and if I need to make a change to one, I need to make changes to the other as well.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
};
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With