9
\$\begingroup\$

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.

\$\endgroup\$
2
\$\begingroup\$

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>>.

\$\endgroup\$
  • \$\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, see FutureWithContinuations::wait. The fact that the class uses std::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 May 21 '15 at 9:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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