In the future, future will have a .then operator that lets you chain tasks.
Lacking it we can write it.
// complete named operator library in about a dozen lines of code:
namespace named_operator {
template<class D>struct make_operator{ constexpr make_operator() {}; };
template<class T, class O> struct half_apply { T&& lhs; };
template<class Lhs, class Op>
half_apply<Lhs, Op> operator*( Lhs&& lhs, make_operator<Op> ) {
return {std::forward<Lhs>(lhs)};
}
template<class Lhs, class Op, class Rhs>
decltype(auto) operator*( half_apply<Lhs, Op>&& lhs, Rhs&& rhs )
{
return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
}
}
// create a named operator then:
namespace then_ns {
static const struct then_t:named_operator::make_operator<then_t> {} then{};
namespace details {
template<size_t...Is, class Tup, class F>
auto invoke_helper( std::index_sequence<Is...>, Tup&& tup, F&& f )
->decltype(std::forward<F>(f)( std::get<Is>(std::forward<Tup>(tup))... ))
{
return std::forward<F>(f)( std::get<Is>(std::forward<Tup>(tup))... );
}
}
// first overload of A *then* B handles tuple and tuple-like return values:
template<class Tup, class F>
auto named_invoke( Tup&& tup, then_t, F&& f )
-> decltype( details::invoke_helper( std::make_index_sequence< std::tuple_size<std::decay_t<Tup>>{} >{}, std::forward<Tup>(tup), std::forward<F>(f) ) )
{
return details::invoke_helper( std::make_index_sequence< std::tuple_size<std::decay_t<Tup>>{} >{}, std::forward<Tup>(tup), std::forward<F>(f) );
}
// second overload of A *then* B
// only applies if above does not:
template<class T, class F>
auto named_invoke( T&& t, then_t, F&& f, ... )
-> std::result_of_t< F(T) >
{
return std::forward<F>(f)(std::forward<T>(t));
}
// *then* with a future; unpack the future
// into a call to f within an async:
template<class X, class F>
auto named_invoke( std::future<X> x, then_t, F&& f )
-> std::future< std::decay_t<decltype( std::move(x).get() *then* std::declval<F>() )> >
{
return std::async( std::launch::async,
[x = std::move(x), f = std::forward<F>(f)]() mutable {
return std::move(x).get() *then* std::move(f);
}
);
}
// void future, don't try to pass void to f:
template<class F>
auto named_invoke( std::future<void> x, then_t, F&& f )
-> std::future< std::decay_t<decltype( std::declval<F>()() )> >
{
return std::async( std::launch::async,
[x = std::move(x), f = std::forward<F>(f)]() mutable {
std::move(x).get();
return std::move(f)();
}
);
}
}
using then_ns::then;
see, that wasn't that hard.
a *then* f
, if a
is a tuple (or pair or array), will invoke f
with the contents of a
.
If a
isn't tuple-like, or f
doesn't accept the contents of a
that way, it invokes f
with a
.
If a
is a future, it instead creates a new async future that consumes a.get()
using *then*
.
Live example.
Suppose you want to increase an atomic int when the file is saved:
std::vector<std::future<void>> saveFileTasks;
for (int n = 0; n < p.size(); ++n)
{
saveFileTasks.push_back(
std::async(std::launch::async, [filename]{
saveFile(filename);
})
);
}
std::atomic<int> count;
for (auto &e : saveFileTasks) {
e = std::move(e) *then* [&count]{
++count;
});
}
Naturally this can all be done without the named operator *then*
style syntax, but what is the fun of that?
If the first async returns a tuple, the second one can either take it as a tuple or as unpacked "flat" arguments.
std::future::wait
do the job? What's the point of runningsave_file
andlong_operation
in separate threads if you have to wait until file is saved anyways? – Protuberant