I am trying to make a MyWrapper
struct which has a template function called set()
. The set()
function can take a callable as an argument. In my 3 use cases, they are:
- lambda
[...]() -> void{...}
(move capturing one as well) std::function<void()>
- any callable class
I am not understanding how I can store the handle passed in as an argument of set()
as a member variable in MyWrapper
.
I don’t want to use classes / base classes with virtual
nor std::function<void()>
inside of MyWrapper
, nor do I want to make MyWrapper
a template struct. Is there a way to do it?
I have been trying with the below code, but I am not understanding how to resolve saving the handle to savedHandler
. Is my approach completely incorrect?
I am inspired from usage of boost asio timer functions, like async_wait()
, which can take any kind of callable. But I am not understanding how to replicate it.
https://coliru.stacked-crooked.com/a/67e9858d20b7dc73
#include <iostream>
#include <memory>
#include <utility>
struct MyWrapper{
template <typename T>
void set(T&& handle){
using HandlerType = std::decay_t<T>;
savedHandler = std::make_unique<HandlerWrapper<HandlerType>>(std::forward<T>(handle));
}
void invoke(){
if (savedHandler){
savedHandler->invoke();
}
}
template<typename Callable>
class HandlerWrapper {
public:
explicit HandlerWrapper(Callable&& handler)
: handler_(std::move(handler)) {}
void invoke() {
handler_();
}
private:
Callable handler_;
};
std::unique_ptr<HandlerWrapper<void()>> savedHandler;
};
struct RandomStruct
{
RandomStruct(std::unique_ptr<int>&& ptr)
: ptr_(std::move(ptr)){}
void operator()()
{
std::cout << "Handler invoked with value: " << *ptr_ << std::endl;
}
std::unique_ptr<int> ptr_;
};
int main() {
MyWrapper op;
// use case 1
op.set([ptr = std::make_unique<int>(2)]() {
std::cout << "Handler invoked with value: " << *ptr << std::endl;
});
op.invoke();
// use case 2
RandomStruct rs(std::make_unique<int>(2));
op.set(std::move(rs));
op.invoke();
return 0;
}
9
I would really recommend you use std::move_only_function, or for C++17 find another verison of it online, it also goes by the name any_invocable
, and available in multiple libraries online including google’s abseil, even asio has a version called any_completetion_handler
A simplified implementation of std::move_only_function
based on your code is as follows, the basic idea is that we type-erase everything behind void pointers and cast it back to our type on invocation or deletion.
#include <iostream>
#include <memory>
#include <utility>
struct MyWrapper {
using erased_unique_ptr = std::unique_ptr<void, void(*)(void*)>;
template <typename T>
void set(T&& handle) {
using HandlerType = std::decay_t<T>;
m_object = erased_unique_ptr{
static_cast<void*>(new HandlerType{std::forward<T>(handle)}),
+[](void* ptr) {
delete static_cast<T*>(ptr);
} };
m_func = +[](void* obj)
{
static_cast<T*>(obj)->operator()();
};
}
using void_func_t = void(*)();
void set(void_func_t handle) {
m_object = erased_unique_ptr{
reinterpret_cast<void*>(handle),
+[](void*) {} };
m_func = +[](void* obj)
{
reinterpret_cast<void_func_t>(obj)();
};
}
MyWrapper() : m_object{ nullptr,+[](void*) {} }, m_func{ nullptr } {}
MyWrapper(MyWrapper&&) = default;
MyWrapper& operator=(MyWrapper&&) = default;
void invoke() {
if (m_object) {
m_func(m_object.get());
}
}
using func_type = void(*)(void*);
erased_unique_ptr m_object;
func_type m_func;
};
struct RandomStruct
{
RandomStruct(std::unique_ptr<int>&& ptr)
: ptr_(std::move(ptr)) {}
void operator()()
{
std::cout << "Handler invoked with value: " << *ptr_ << std::endl;
}
std::unique_ptr<int> ptr_;
};
void hello()
{
std::cout << "free function" << 'n';
}
int main() {
MyWrapper op;
// use case 1
op.set([ptr = std::make_unique<int>(2)]() {
std::cout << "Handler invoked with value: " << *ptr << std::endl;
});
op.invoke();
// use case 2
RandomStruct rs(std::make_unique<int>(2));
op.set(std::move(rs));
op.invoke();
op.set(hello);
op.invoke();
return 0;
}
godbolt demo
std::move_only_function
and its different implementations offers a small buffer optimization, while others allow using custom allocators, which we just ignore in this super simplified implementation.