34

I have a few questions about the implementation of the function then() in Herb Sutter's talk. This function is used to chain asynchronous operations, the parameter f is a future from one operation and the parameter w is the 'work' for this operation (lambda).

template <typename Fut, typename Work>
auto then(Fut f, Work w) -> future<decltype(w(f.get()))>
{
    return async([=]{ w(f.get()); });
}

An example of application would be:

    std::future<int> f = std::async([]{
        std::this_thread::sleep_for(std::chrono::microseconds(200));
        return 10;
    });

    auto f2 = then(std::move(f), [](int i){
        return 2 * i;
    });

The main thread spawns the tasks but does not wait for any of them to finish.

Firstly, future<T> does not have a copy constructor. This means, that the suggested implementation can be only used with shared_future<T> unless we change the call to async() to move the future into the lambda. This SO question suggested a way of doing it but it seems too complicated. I re-implemented the function and I am wondering whether my code is correct or whether I missed something...

Secondly, the future that is passed to the then() function might be void so we actually need 2 implementations of then(), right? One for futures returning T and one for futures returning void.

Lastly, should the lambda inside the body of then() not have a return statement so that we can actually return the value back? Without the return statement, then returns future<void>, right?

I tried to address the above points and this is what I came up with. Is it correct?

template <typename T, typename Work>
auto then(future<T> f, Work w) -> future<decltype(w(f.get()))>
{
    return async([](future<T> f, Work w)
                      { return w(f.get()); }, move(f), move(w));
}

template <typename Work>
auto then(future<void> f, Work w) -> future<decltype(w())>
{
    return async([](future<void> f, Work w)
                      { f.wait(); return w(); }, move(f), move(w));
}
River
  • 8,585
  • 14
  • 54
  • 67
Roman Kutlak
  • 2,684
  • 1
  • 19
  • 24
  • 1
    What do you mean by "correct?" – Robert Harvey Jan 23 '13 at 21:42
  • By "correct" I mean logically correct. The code compiles fine (clang 3.3 + libc++) but that does not mean it does what it is supposed to do. – Roman Kutlak Jan 23 '13 at 21:44
  • 2
    No, you do not need another version because of void - it is perfectly fine to do something like this: `void f(); void g() { return f(); }`. – sdkljhdf hda Jan 26 '13 at 04:00
  • 1
    The problem is not the `return f();` part but the `w(f.get())` part. You are trying to pass a `void` parameter to a function that does not take any parameters. E.g., `void f(); void g(); [](){ return f(g()); }` – Roman Kutlak Jan 26 '13 at 20:21
  • There is no such thing as a void parameter. It's a keyword that represent *not* a parameter. – Tim Jan 27 '13 at 18:12
  • My point exactly. That is the reason for the second version of `then()`. – Roman Kutlak Jan 28 '13 at 08:52
  • I'd write a `future`->functor adapter, and a `chain` function that handles the `void` return value, rather than writing two `then`. – Yakk - Adam Nevraumont Jan 30 '13 at 15:50
  • Is it maybe interesting to have a look of the implementation of [` then` in boost thread](http://www.boost.org/doc/libs/1_53_0/doc/html/thread/synchronization.html#thread.synchronization.futures)? – Stephan Feb 14 '13 at 08:19

3 Answers3

6

The problem with this approach to .then() is that you spawn 2 threads (that is costly) simultaneously, and second of them would block on its future.get/wait (if the first one would run long enough, of course) So, its better to use the work queue, to serialize the jobs execution order (and re-cycle the existing threads). Just look for a good Thread pool pattern implementation

barney
  • 2,172
  • 1
  • 16
  • 25
4

In order to simplify the interface, I would "hide" the void problem inside the implementation, similarly to what Herb did with his concurrent<T> implementation. Instead of having 2 then implementations, declare a helper function get_work_done with 2 implementations:

template <typename T, typename Work>
auto get_work_done(future<T> &f, Work &w)-> decltype(w(f.get()))
{return w(f.get());}

template <typename Work>
auto get_work_done(future<void> &f, Work &w)-> decltype(w())
{f.wait(); return w();}

And then let template parameter detection take care of the rest:

template <typename T, typename Work>
auto then(future<T> f, Work w) -> future<decltype(w(f.get()))>
{
    return async([](future<T> f, Work w)
                      { return get_work_done(f,w); }, move(f), move(w));
}
eladidan
  • 2,634
  • 2
  • 26
  • 39
  • 1
    I am not sure this adds any value. I guess if there was more work in the ``then`` function it would make sense but as it is there is no need for the extra function. – Roman Kutlak Feb 19 '13 at 11:28
  • The idea is simplifying the interface and leaving the implementation details "inside". But since it's just 2 overloads, and very short code it's not absolutely necessary – eladidan Feb 19 '13 at 12:07
1

no, it is not correct. if you pass return value of .get() to continuation, it will not be able to handle exception, propagated from .get(). you have to pass future to continuation, and call .get() manually, just like in boost.thread

pal
  • 618
  • 4
  • 9
  • No, if `.get()` throws from inside the new lambda, the future created from *that* will correctly hold that exception. – Xeo Feb 20 '13 at 00:24
  • no, this will handle exception at the end of chain, skipping all steps in between, while without .then() you can handle exceptions after every step – pal Feb 20 '13 at 02:03
  • Eh, that's the whole idea behind `.then` - *if* the work was successful, *then* do some other work. You might argue that it would be good to be able to pass an "else" function, so you can handle exceptions too, and I won't argue against that. – Xeo Feb 20 '13 at 07:51
  • i'm pretty sure the idea is to do some work after previous step is finished either by return or by exception. just like in sequential code without .then(). i argue that it would be good to have a chance to handle exception in continuation ( for example by substituting default value instead of result of .get() ). also i predict you will have some fun implementing .else() – pal Feb 20 '13 at 11:53
  • C++14: f1.then([](future f) { return f.get().to_string(); // here .get() won’t block }); Otherwise there is no way to capture the error – TimW Oct 15 '13 at 16:10