While playing with the concurrency features in C++11 I noticed that there wasn't any support for continuations. I wanted to develop something similar to Tasks in The Parallel Patterns Library (PPL), where a Task can run asynchronously and execute a continuation upon completion using the 'then' member function.
I designed my class around std::future
and std::async
, which provides the asynchronous processing. The function to run asynchronously accepts any number of parameters and is called using std::async
:
template<typename T>
class FutureWithContinuations final
{
public:
typedef std::function<void(T)> ContinautionSignature;
template<typename Function, typename... ArgTypes>
FutureWithContinuations(Function&& futureFunction, ArgTypes&&... args)
: m_continuations()
, m_future()
{
// Let the concurrency runtime decide whether to run asynchronously or to defer running of the future
auto future = std::async(std::launch::any, std::forward<Function>(futureFunction), std::forward<ArgTypes>(args)...);
m_future = future.share();
}
template<typename Function>
void then(const Function& continuationFunction)
{
auto continuation = make_unique_continuation(continuationFunction, m_future);
m_continuations.push_back(std::move(continuation));
}
void wait()
{
if (!m_future.valid())
throw std::future_error(std::future_errc::no_state);
m_future.wait();
for (const auto& continuation : m_continuations)
{
continuation->execute();
}
m_continuations.clear();
}
T get()
{
return m_future.get();
}
private:
std::vector<std::unique_ptr<IContinuation>> m_continuations;
std::shared_future<T> m_future;
};
I have two types of continuations, one for when the asynchronous operation produces a result and one for when it doesn't. Both implement an interface that specifies the API for the FutureAndContinuations
class to use when dealing with continuations:
class IContinuation
{
public:
IContinuation(){}
virtual ~IContinuation() = default;
virtual void execute() = 0;
};
Template specialisation is used along with std::enable_if
and the template parameter feature that 'substitution failure is not an error' (SFINAE) in order to differentiate between continuations that don't act on a future result (continuation function takes no parameters):
template<typename FutureReturnType, typename ContinuationFunction, typename SFINAE = void>
class Continuation final : public IContinuation
{
public:
Continuation(const ContinuationFunction& continuationFn, const std::shared_future<FutureReturnType>& sharedFuture)
: IContinuation()
, m_continuationFn(continuationFn)
, m_sharedFuture(sharedFuture){}
void execute() override
{
m_continuationFn();
}
private:
ContinuationFunction m_continuationFn;
std::shared_future<FutureReturnType> m_sharedFuture;
};
And continuations that do act on a future result (continuation function takes one parameter that is the result of the std::future
):
template<typename FutureReturnType, typename ContinuationFunction>
class Continuation<FutureReturnType, ContinuationFunction, typename std::enable_if<!std::is_void<FutureReturnType>::value>::type> final : public IContinuation
{
public:
Continuation(const ContinuationFunction& continuationFn, const std::shared_future<FutureReturnType>& sharedFuture)
: IContinuation()
, m_continuationFn(continuationFn)
, m_sharedFuture(sharedFuture){}
void execute() override
{
m_continuationFn(m_sharedFuture.get());
}
private:
ContinuationFunction m_continuationFn;
std::shared_future<FutureReturnType> m_sharedFuture;
};
I have a helper function for creating continuations that the user can call without having to specify the tempalte parameter types as they can be deduced:
template<typename FutureReturnType, typename ContinuationFunction>
std::unique_ptr<IContinuation> make_unique_continuation(const ContinuationFunction& continuationFunction, const std::shared_future<FutureReturnType>& sharedFuture)
{
return std::unique_ptr<IContinuation>(new Continuation<FutureReturnType, ContinuationFunction>(continuationFunction, sharedFuture));
}
The code was developed in Visual Studio 2013 and I used MSTest in order to validate the functionality. The code contains explicit disabling of copying and moving in each class using delete but I have excluded those from the code snippets for the sake of brevity. All of the code including the tests is available on Bitbucket.
1 Answer 1
Not sure what kind of feedback you're looking for here.
What you've implemented is not equivalent to what's usually called .then()
. The usual semantics of .then()
allow you to do things like
std::future<int> f = std::async([]{ return 1; });
std::future<int> g = f.then([](std::future<int> x){ return x.get() + 1; });
int v = g.get();
assert(v == 1+1);
What you've got, on the other hand, allows you to do something like
std::future<int> f = std::async([]{ return 1; });
f.then([](int x){ printf("%d\n", x); return x + 1; });
int v = f.get(); // prints "1"
assert(v == 1); // v is not 2
I don't really understand why you'd want the latter semantics. Do you find them useful?
typedef std::function<void(T)> ContinautionSignature;
You misspelled "ContinuationSignature" here; but it doesn't matter, because the type is unused. Remove it.
Your class Continuation
is merely a type-erased wrapper around a callable. We have a name for that in C++11 and later: it's called std::function
. Consider:
template<typename FutureReturnType, typename ContinuationFunction>
std::function<void()> make_unique_continuation(
const ContinuationFunction& continuationFunction,
const std::shared_future<FutureReturnType>& sharedFuture)
{
return [=]() mutable { continuationFunction(sharedFuture.get()); };
}
If you do this, about half of your code simply disappears!
(You'll still need to specialize and/or overload the above function a little bit to deal with void
, but it looks like you've got the right ideas about that already.)
You may already be aware that in C++14, with proper library support, you can rewrite typename std::enable_if<!std::is_void<FutureReturnType>::value>::type
as simply std::enable_if_t<!std::is_void_v<FutureReturnType>>
.
-
\$\begingroup\$ Thanks for your response. The feedback that I am looking for issues to be highlighted and improvements/alternatives to be suggested. I don't agree with your observation at the top. The
FutureWithContinuations::then
function doesn't execute a callable instantly. It stores it for execution after waiting for the future, seeFutureWithContinuations::wait
. The fact that the class usesstd::future
is an implementation detail. A user only needs to know the return type generated from the future function, that is then passed as an argument to the continuation. The tests demonstrate usage. \$\endgroup\$Eóin Ó'Flynn– Eóin Ó'Flynn2015年05月21日 09:28:40 +00:00Commented May 21, 2015 at 9:28