6
\$\begingroup\$

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
\$\endgroup\$
2
  • \$\begingroup\$ If anyone needs clarification on anything, please leave a comment. \$\endgroup\$
    – cr_oag
    Commented Oct 11, 2015 at 15:10
  • \$\begingroup\$ I've added a second (clearer) example to better illustrate the type erasure. \$\endgroup\$
    – cr_oag
    Commented Oct 11, 2015 at 19:53

1 Answer 1

2
\$\begingroup\$

Well, the deferred_invoker can be radically simplified and generified, use Lambdas, std::ref, a maker-function, and one intermediate type-erasure-step for best flexibility:

#include <exception>
#include <future>
#include <type_traits>
#include <utility>

struct invokable
{
    virtual ~invokable() = default;
    virtual void invoke() = 0;
protected:
    invokable() = default;
    invokable(invokable&&) = default;
    invokable& operator=(invokable&&) noexcept = default;
};

template<class R, class L = void>
struct deferred_invoker final : deferred_invoker<R>
{
    deferred_invoker(L&& l) : lambda(std::move(l)) {}
    deferred_invoker(const L& l) : lambda(l) {}
    deferred_invoker(deferred_invoker&&) = default;
    deferred_invoker& operator=(deferred_invoker&&) = default;
    void invoke() override {
        try {
            this->invoke_helper((R*)0);
        } catch(...) {
            this->result.set_exception(std::current_exception());
        }
    }
private:
    L lambda;
    template <class ret> auto invoke_helper(ret*)
    -> std::enable_if_t<std::is_same<void, ret>::value>
    { this->lambda(); this->result.set_value(); }
    template <class ret> auto invoke_helper(ret*)
    -> std::enable_if_t<!std::is_same<void, ret>::value>
    { this->result.set_value(this->lambda()); }
};

template<class R>
struct deferred_invoker<R> : invokable
{
    using f_result = R;
    std::future<R> get_future() { return this->result.get_future(); }
protected:
    std::promise<R> result;
};

template<class F, class A1, class... ARGS>
auto make_deferred_invoker(F&& f, A1&& a1, ARGS&&... args) {
    auto lambda = [=]{ return std::ref(f)(a1, args...); };
    return deferred_invoker< std::decay_t<decltype(lambda())>,
        std::decay_t<decltype(lambda)> >{std::move(lambda)};
}

// Avoid re-packaging any callable
template<class F>
auto make_deferred_invoker(F&& lambda) {
    return deferred_invoker<std::decay_t<decltype(lambda())>,
        std::decay_t<decltype(lambda)>>{std::move(lambda)};
}

BTW: You should take more care with the rule-of-3.

Also, the manager-class in the second example is leaking, use std::unique_ptrs or some such.

A demo on coliru: http://coliru.stacked-crooked.com/a/08d85370e66675cf

\$\endgroup\$
9
  • \$\begingroup\$ You are definitely correct about the rule of 3/5. I overlooked that. \$\endgroup\$
    – cr_oag
    Commented Oct 11, 2015 at 21:47
  • \$\begingroup\$ Well, now there are two levels of type-erasure: Max, only useful for calling invoke and delete. Intermediate, all but moving works. No type-erasure. \$\endgroup\$ Commented Oct 12, 2015 at 2:06
  • \$\begingroup\$ I've done some simple timing tests and yours is faster when copying POD's. However, there must be some sort of issue with the deductions going on, because even if the function has a const&, it is copied, making it a lot slower. \$\endgroup\$
    – cr_oag
    Commented Oct 12, 2015 at 3:38
  • \$\begingroup\$ Well, all function-arguments are copied or moved into the lambda. Do you want some not to be copied? \$\endgroup\$ Commented Oct 12, 2015 at 3:45
  • \$\begingroup\$ Yeah, perfect forwarding is important for that reason. Also, there seems to be an issue when calling your submit() with functions that have void return types. Try with this signature void f( int );. \$\endgroup\$
    – cr_oag
    Commented Oct 12, 2015 at 4:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.