1. Description
1.1. Functionality
The objective of these types is to provide type erasure for any function while maintaining the ability to provide a return value through a combination of std::promise<T>
and its associated std::future<T>
.
1.2. Motivation
This is useful for any sort of dispatch manager, a thread pool that you can submit tasks to being the actual target; where a task is any function with any parameters and return type.
2. Implementation
2.1. deferred_invoker.h
This is the main functionality. Type erasure is provided through the invoker_base
base class, which simply has a virtual member function that is called when the function has to be invoked.
The specializations will then know how to deal with the invocation and whether they have to save a result. They also take care of ensuring the correct initialization arguments required for the call are initialized and saved in a tuple. This applies for template parameter F
which is any function.
A simple tag dispatch system takes care of void
return types. I've omitted a third implementation of deferred_invoker<F>
for lambda and functor types to focus the review. It's basically the same style as the other two (possible design issue?), but it takes a copy of the lambda/functor and the arguments, instead of a pointer to the function.
#ifndef OAG_DEFERRED_INVOKER_H
#define OAG_DEFERRED_INVOKER_H
#include <memory>
#include "function_traits.h"
namespace oag
{
template <typename Tuple>
struct bare_type_tuple;
template <typename... Args>
struct bare_type_tuple<std::tuple<Args...>>
{
using type = std::tuple<std::decay_t<Args>...>;
};
template <typename Tuple>
using bare_type_tuple_t = typename bare_type_tuple<Tuple>::type;
}
namespace oag
{
class invoker_base
{
public:
virtual ~invoker_base() {}
virtual void invoke() = 0;
};
template
<
typename F,
bool for_member_function = oag::function_traits<F>::is_member_fn
>
class deferred_invoker;
/*
FREE FUNCTION SPECIALIZATION
*/
template <typename F>
class deferred_invoker<F, false> : public invoker_base
{
public:
using f_traits = oag::function_traits<F>;
using f_return = typename f_traits::return_type;
using f_params = typename f_traits::parameter_tuple;
using f_ptr = typename f_traits::function_pointer;
public:
template <typename... Args>
deferred_invoker( f_ptr f, Args&&... args ) :
function_{ f },
arguments_{ std::forward<Args>( args )... }
{
static_assert( sizeof...(Args) == f_traits::arity,
"invoker: missing arguments for call" );
static_assert( std::is_same<
bare_type_tuple_t<std::tuple<Args...>>, // args
bare_type_tuple_t<f_params>>::value, // f_params
"invoker: type mismatch for function call.");
}
void invoke() override
{
invoke_impl( std::make_index_sequence<f_traits::arity>{},
typename std::is_same<void, f_return>::type{} );
}
std::future<f_return> get_future()
{
return result_.get_future();
}
private:
template <std::size_t... Indices>
inline void invoke_impl( std::index_sequence<Indices...>, std::false_type )
{
result_.set_value( std::move( ( *function_ )(
std::forward<std::tuple_element_t<Indices, f_params>>(
std::get<Indices>( arguments_ ) )... ) ) );
}
template <std::size_t... Indices>
inline void invoke_impl( std::index_sequence<Indices...>, std::true_type )
{
( *function_ )( std::forward<std::tuple_element_t<Indices, f_params>>(
std::get<Indices>( arguments_ ) )... );
}
private:
f_ptr function_;
f_params arguments_;
std::promise<f_return> result_;
};
/*
MEMBER FUNCTION SPECIALIZATION
*/
template <typename F>
class deferred_invoker<F, true> : public invoker_base
{
public:
using f_traits = oag::function_traits<F>;
using f_caller_t = typename f_traits::class_type;
using f_return = typename f_traits::return_type;
using f_params = typename f_traits::parameter_tuple;
using f_ptr = typename f_traits::function_pointer;
public:
template <typename... Args>
deferred_invoker( f_ptr f, f_caller_t* c, Args&&... args ) :
function_{ f },
caller_{ c },
arguments_{ std::forward<Args>( args )... }
{
static_assert( sizeof...( Args ) == f_traits::arity,
"invoker: missing arguments for call" );
static_assert( std::is_same<
bare_type_tuple_t<std::tuple<Args...>>, // args
bare_type_tuple_t<f_params>>::value, // f_params
"invoker: type mismatch for function call." );
}
void invoke() override
{
invoke_impl( std::make_index_sequence<f_traits::arity>{},
typename std::is_same<void, f_return>::type{} );
}
std::future<f_return> get_future()
{
return result_.get_future();
}
private:
template <std::size_t... Indices>
inline void invoke_impl( std::index_sequence<Indices...>, std::false_type )
{
result_.set_value( std::move( ( caller_->*function_ )(
std::forward<std::tuple_element_t<Indices, f_params>>(
std::get<Indices>( arguments_ ) )... ) ) );
}
template <std::size_t... Indices>
inline void invoke_impl( std::index_sequence<Indices...>, std::true_type )
{
( caller_->*function_ )(
std::forward<std::tuple_element_t<Indices, f_params>>(
std::get<Indices>( arguments_ ) )... );
}
private:
f_caller_t* caller_;
f_ptr function_;
f_params arguments_;
std::promise<f_return> result_;
};
}
#endif // OAG_DEFERRED_INVOKER_H
2.2. function_traits.h
These are simple template class specializations that provide the required function traits for the implementation of deferred_invoker<F>
. I realize that specializations for volatile
and const volatile
functions are required.
#ifndef OAG_FUNCTION_TRAITS_H
#define OAG_FUNCTION_TRAITS_H
#include <tuple>
namespace oag
{
template <typename F>
struct function_traits;
template <typename F>
struct function_traits : function_traits<decltype( &F::operator() )>
{
static bool constexpr is_functor = true;
};
template <typename FReturn, typename... FArgs>
struct function_traits<FReturn(*)( FArgs... )> : function_traits<FReturn( FArgs... )>
{
};
template <typename FReturn, typename... FArgs>
struct function_traits<FReturn( FArgs... )>
{
using function_pointer = FReturn(*)( FArgs... );
using return_type = FReturn;
using parameter_tuple = std::tuple<FArgs...>;
template <std::size_t Index>
using parameter = std::tuple_element_t<Index, std::tuple<FArgs...>>;
static std::size_t constexpr arity = std::tuple_size<std::tuple<FArgs...>>::value;
static bool constexpr is_member_fn = false;
static bool constexpr is_functor = false;
};
template <typename C, typename FReturn, typename... FArgs>
struct function_traits<FReturn( C::* )( FArgs... )> : function_traits<FReturn( FArgs... )>
{
using function_pointer = FReturn( C::* )( FArgs... );
using class_type = C;
static bool constexpr is_member_fn = true;
static bool constexpr is_functor = false;
};
template <typename C, typename FReturn, typename... FArgs>
struct function_traits<FReturn( C::* )( FArgs... ) const> : function_traits<FReturn( C::* )( FArgs... )>
{
using function_pointer = FReturn( C::* )( FArgs... ) const;
};
}
#endif // !OAG_FUNCTION_TRAITS_H
3. Sample usage
Here's some sample usage to show how a user can get the return values. Basically, the user would send in their function/functor to the dispatcher and deferred_invoker<F>
would take care of the rest; the dispatcher would return the std::future<T>
from its submit-a-function function.
3.1. Example 1
This example is pretty much just a test of the template deduction rules and shows how you can use a collection to erase types. The main feature is that you can have a collection of functions that all have different signatures and return types.
#include <iostream>
#include <string>
#include <future>
#include "deferred_invoker.h"
int max( int* a, int b )
{
return *a < b ? b : *a;
}
void print( std::string const& s )
{
std::cout << s;
}
struct C
{
C( std::string&& s ) : s_{ std::move( s ) }
{}
double string33() const
{
return s_.size() * 0.33;
}
std::string s_;
};
int main()
{
int* p_a{ new int{ 17 } };
oag::deferred_invoker<decltype( ::max )> i1{ ::max, p_a, 37 };
std::string s{ "hello_world\n" };
oag::deferred_invoker<decltype( print )> i2{ print, s };
C c{ "str" };
oag::deferred_invoker<decltype( &C::string33 )> i3{ &C::string33, &c };
std::future<int> f1 = i1.get_future();
std::future<void> f2 = i2.get_future();
std::future<double> f3 = i3.get_future();
std::vector<oag::invoker_base*> invoke_list{ &i1, &i2, &i3 };
for ( auto invoker : invoke_list )
invoker->invoke();
std::cout << f1.get() << '\n';
std::cout << f3.get() << '\n';
}
3.2. Example 2
This example displays a more "real-world" use. Instead of having the submit()
function call invoke()
, that would be done by threads consuming from the tasks_
data member. Since this is an example, I've provided a simple submit()
function that works only for free functions; overloads can easily be added to deal with functors and member functions.
#include <iostream>
#include <string>
#include <future>
#include <vector>
#include "deferred_invoker.h"
class task_manager
{
public:
template <typename F, typename... FArgs>
std::future<typename oag::function_traits<F>::return_type>
submit( F&& f, FArgs&&... f_args )
{
oag::deferred_invoker<F>* p_task = new oag::deferred_invoker<F>{
std::forward<F>( f ), std::forward<FArgs>( f_args )... };
tasks_.emplace_back( p_task );
// note: this call is purely for example;
// it would normally be done by the consumer thread.
p_task->invoke();
return p_task->get_future();
}
std::vector<oag::invoker_base*> tasks_;
};
int add( int a, int b )
{
return a + b;
}
std::string concat( std::string const a, std::string const b )
{
return a + b;
}
int main()
{
task_manager tm;
// notice how we can add two complete different functions and still
// get their return value.
auto result1 = tm.submit( &add, 1, 1 );
auto result2 = tm.submit(
&concat, std::string("hello"), std::string( " world" ) );
std::cout << result1.get() << '\n';
std::cout << result2.get() << '\n';
}
4. Review goals
- Implementation - I am very open to somehow simplify the
deferred_invoker<T>
type. Do I really need three specializations? - Efficiency
- Clarity
- Anything else