Yonghui
Yonghui

Reputation: 222

C++11/14 how to create async task using function that return type is void or others

I want to write a simple task class that similar to ppltasks.

I want my task class can create task as following:

1.CreateTask([=]{}).then([=]{...});
2.CreateTask([=]{ return X; }).then([=](UnknownType X){...});

But the function I want to run in task may have different type:void, float, string etc. And here is the key point:

auto val = func();    // If func is a void function
prms->set_value(val); // here should be: func(); prms->set_value();

How can I handle void function and other type function using same CreateTask?

Following is my full code of task class:

template<typename TType>
class Task
{
public:
    Task(){};
    std::future<TType> mTask;
    template<typename F>
    auto then(F func)->Task<decltype(func(mTask.get()))>
    {
        Task<decltype(func(mTask.get()))> task;
        auto prms = make_shared<promise<decltype(func(mTask.get()))>>();
        task.mTask = prms->get_future();
        thread th([=]
        {
            auto val = func(mTask.get()); //
            prms->set_value(val);
        });
        th.detach();
        return task;
    });
};

inline void CreateTask(F func) -> Task<decltype(func())>
{
    Task<decltype(func())> task;
    auto prms = make_shared<promise<decltype(func())>>();
    task.mTask = prms->get_future();
    thread th([=] 
    {
        auto val = func();
        prms->set_value(val);
    });
    th.detach();
    return task;
}

Upvotes: 2

Views: 1780

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275220

Lets attack this via separation of concerns.

First, the problem of pipeing the output of one invokable to another:

template<class Index>
auto get_nth( Index ) {
  return [](auto&&...args)noexcept(true)->decltype(auto) {
    return std::get<Index::value>(
      std::forward_as_tuple( decltype(args)(args)... )
    );
  };
}

template<class F>
struct pipeable_t;

template<class F>
pipeable_t<std::decay_t<F>> make_pipe(F&&);

struct is_pipeable {
    template<
        class Lhs, class Rhs,
        std::enable_if_t< 
            std::is_base_of<is_pipeable, std::decay_t<Lhs>>{}
            || std::is_base_of<is_pipeable, std::decay_t<Rhs>>{}
        , int> = 0
    >
    friend
    auto operator|( Lhs&& lhs, Rhs&& rhs ) {
        auto pipe_result=
            [lhs = std::forward<Lhs>(lhs), rhs=std::forward<Rhs>(rhs)]
            (auto&&...args)mutable ->decltype(auto) 
            {
                auto value_is_void = std::is_same< decltype( lhs(decltype(args)(args)...) ), void >{};
                auto pipe_chooser = get_nth( value_is_void );
                auto pipe_execution = pipe_chooser(
                    [&](auto&& lhs, auto&& rhs)->decltype(auto){ return rhs( lhs( decltype(args)(args)... ) ); },
                    [&](auto&& lhs, auto&& rhs)->decltype(auto){ lhs( decltype(args)(args)... ); return rhs(); }
                );
                return pipe_execution( lhs, rhs );
            };
        return make_pipe( std::move(pipe_result) );
    }
};

template<class F>
struct pipeable_t:is_pipeable {
    F f;
    pipeable_t( F fin ):f(std::forward<F>(fin)) {}
    template<class...Args>
    auto operator()(Args&&...args) const {
        return f( std::forward<Args>(args)... );
    }
    template<class...Args>
    auto operator()(Args&&...args) {
        return f( std::forward<Args>(args)... );
    }
};

template<class F>
pipeable_t<std::decay_t<F>> make_pipe(F&& f) { return {std::forward<F>(f)}; }

template<class T>
auto future_to_factory( std::future<T>&& f ) {
    return [f=std::move(f)]() mutable { return f.get(); };
}

template<class T>
auto make_pipe( std::future<T>&& f ) {
    return make_pipe( future_to_factory( std::move(f) ) );
}

we can make_pipe( some lambda ) and it can now be fed into more stuff.

A pipe is executable and pipeable itself. We can make anything executable a pipe, and we can make a std::future into a pipe.

Once we have this, we rewrite your Task in terms of it:

struct dispatch_via_async {
    template<class F>
    auto operator()(F&& f)const {
        return std::async( std::launch::async, std::forward<F>(f) );
    }
};

template<class T, class Dispatch=dispatch_via_async>
class Task;

template<class F, class D=dispatch_via_async>
using task_type = Task<std::decay_t<std::result_of_t<F&&()>>, D>;

template<class F, class D=dispatch_via_async>
task_type<F, D> CreateTask(F&& func, D&& d={});

template<class T, class Dispatch>
class Task :
    public is_pipeable, // why not?
    private Dispatch
{
    std::future<T> mTask;

    Dispatch& my_dispatch() { return *this; }
public:
    T operator()() { return mTask.get(); }
    Task(){};
    template<class F>
    auto then(F&& func)&&
    {
        return CreateTask(
          make_pipe(std::move(mTask)) | std::forward<F>(func),
          std::move(my_dispatch())
        );
    }

    template<class F, class D=Dispatch>
    explicit Task( F&& func, D&& dispatch={} ):
        Dispatch(std::forward<D>(dispatch))
    {
        mTask = my_dispatch()(
            [func = std::forward<F>(func)]() mutable
            -> decltype(func())
            {
                return func();
            }
        );
    }
};

template<class F, class D>
task_type<F,D> CreateTask(F&& func, D&& d)
{
    return task_type<F,D>( std::forward<F>(func), std::forward<D>(d) );
}

Here we carefully permit the dispatcher (how we get the future) to be passed into a Task if required. The .then'd continuations will use the dispatcher to create the chained futures.

Live example.

As a bonus, you now have a simple streaming chain-of-operations library.

Note that I replaced your thread based implementation with an async based one, because your system failed to properly wait for threads to terminate before program finished, which leads to undefined behavior. You can still replace that Dispatcher with one that uses threads.


In , Regular Void proposal attempts to get rid of the problem. I don't know how well it does the job.


The above doesn't build in current versions of MSVC because MSVC fails to be a proper compiler in that its async uses packaged task, its packaged task stores its tasks in a std function, which results in the async incorrectly requiring that the task being called by copyable as std::function type-erases copy.

This breaks the above code because we store a std::future in our async tasks, which cannot be copied.

We can fix this for a modest cost. The minimal most isolated way to do it is to change future_to_factory to:

template<class T>
auto future_to_factory( std::future<T>&& f ) {
    return [f=std::make_shared<std::future<T>>(std::move(f))]() mutable { return f->get(); };
}

and the code compiles on visual studio.

Upvotes: 3

Related Questions