Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adapting a function that returns std::future<T> to std::future<U>

Suppose I have an asynchronous functional map primitive which takes a std::vector as input and returns a std::future to a Container of my choice as output:

template<class Container, class T, class Function>
std::future<Container> async_map(const std::vector<T>& in, Function f)
{
  return std::async([=]
  {
    Container result(in.size());

    for(size_t i = 0; i < in.size(); ++i)
    {
      result[i] = f(in[i]);
    }

    return result;
  });
} 

I'd like to build an analogous async_for_each function by adapting async_map:

template<class T, class Function>
std::future<void> async_for_each(const std::vector<T>& in, Function f);

The problem is that async_for_each returns std::future<void>, while async_map returns std::future<Container>, and void is not a Container.

I can get something close to what I want by constructing a type which fulfills the Container requirements but ignores assignments to it (empty_container in my initial attempt), but a std::future of this type is still not std::future<void>.

I have the following constraints on my solution:

  • There must be only one implementation of async_map, with the given function signature (i.e., no async_map<void> specialization)
  • There must be only one std::future created (i.e., no .then()-style continuation)

I was hoping there is an efficient way to convert between std::futures of related types (or cast a std::future<T> to std::future<void>), but the answer to this question suggests it is not possible.

Random ideas:

  • Can async_for_each wrap its function in a clever way to solve this problem?
  • Can the type used for Container act like void in async_for_each, but act like Container in async_map?

My initial attempt is below. Is it possible to build what I want given these constraints?

#include <future>
#include <vector>
#include <iostream>

template<class Container, class T, class Function>
std::future<Container> async_map(const std::vector<T>& in, Function f)
{
  return std::async([=]
  {
    Container result(in.size());

    for(size_t i = 0; i < in.size(); ++i)
    {
      result[i] = f(in[i]);
    }

    return result;
  });
}

struct empty_container
{
  empty_container(size_t) {}

  struct empty
  {
    template<class T>
    empty operator=(const T&) const { return empty(); }
  };

  empty operator[](size_t) { return empty(); }
};

template<class Function>
struct invoke_and_ignore_result
{
  Function f;

  template<class T>
  empty_container::empty operator()(T&& x) const
  {
    f(std::forward<T>(x));
    return empty_container::empty();
  }
};

template<class T, class Function>
//std::future<void> async_for_each(const std::vector<T>& in, Function f)
std::future<empty_container> async_for_each(const std::vector<T>& in, Function f)
{
  invoke_and_ignore_result<Function> g{f};

  std::future<empty_container> f1 = async_map<empty_container>(in, g);

  return f1;
}

int main()
{
  std::vector<int> vec(5, 13);

  async_for_each(vec, [](int x)
  {
    std::cout << x << " ";
  }).wait();

  std::cout << std::endl;

  return 0;
}
like image 474
Jared Hoberock Avatar asked May 13 '15 19:05

Jared Hoberock


1 Answers

I think you are using the wrong primitive.

Here I build everything up with a different primitive -- a sink.

A sink can consume data via operator()(T&&)&. It then returns some result via operator()()&&.

Here is a async_sink function:

template<class Container, class Sink>
std::future<std::result_of_t<std::decay_t<Sink>()>>
async_sink(Container&& c, Sink&& sink)
{
  return std::async(
    [c=std::forward<Container>(c), sink=std::forward<Sink>(sink)]
  {
    for( auto&& x : std::move(c) ) {
      sink( x );
    }

    return std::move(sink)();
  });
} 

Here is an implementation of a sink that puts things into a container, then returns it:

template<class C>
struct container_sink_t {
  C c;
  template<class T>
  void operator()( T&& t ){
    c.emplace_back( std::forward<T>(t) );
  }
  C operator()()&&{
    return std::move(c);
  }
};

Here is a sink that takes a function and a sink and composes them:

template<class F, class S>
struct compose_sink_t {
  F f;
  S s;
  template<class T>
  void operator()(T&& t){
    s(
      f(std::forward<T>(t))
    );
  }
  std::result_of_t<S()> operator()()&&{
    return std::move(s)();
  }
};

template<class C, class F>
compose_sink_t<std::decay_t<F>, container_sink_t<C>>
transform_then_container_sink( F&& f ) {
  return {std::forward<F>(f)};
}

Here is a sink that takes a function, calls it, and returns void:

template<class F>
struct void_sink_t {
  F f;
  template<class T>
  void operator()(T&& t)
  {
    f(std::forward<T>(t));
  }
  void operator()() {}
};
template<class F>
void_sink_t<std::decay_t<F>> void_sink(F&&f){return {std::forward<F>(f)}; }

now your map is:

template<class Container, class T, class Function>
std::future<Container> async_map(const std::vector<T>& in, Function f)
{
  return async_sink(
    in,
    transform_then_container_sink<Container>(std::forward<F>(f))
  );
}

and your for_each is:

template<class T, class Function>
std::future<void> async_for_each(const std::vector<T>& in, Function f)
{
  return async_sink(
    in,
    void_sink(std::forward<F>(f))
  );
}

I freely use C++14 features, because they made the code better. You can replace the move-into-container with a copy for a touch less efficiency, and write your own _t aliases.

The above code has not been tested or run, so there are probably bugs in it. There is one issue I'm uncertain of -- can a lambda returning void end with a return void_func() in that context? -- but as that uglyness is in one spot, it can be worked around even if it doesn't work.

like image 99
Yakk - Adam Nevraumont Avatar answered Sep 25 '22 00:09

Yakk - Adam Nevraumont