Boost logo

Boost :

Subject: Re: [boost] Non-allocating constexpr functional continuations-based future-promise
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2014-10-26 17:29:39


On 25 Oct 2014 at 13:00, Bjorn Reese wrote:

> > Given Hartmut's feedback from my previous attempt at a non-allocating
> > future-promise implementation, I now have a second, much more generic
> > demonstration prototype which is based on heterogenous sequences of
> > constexpr functional calls. I think this design may have huge potential.
>
> This is a very nice evolution of your previous promise-future framework.

Thanks.

> > template<class T> struct then_continuation
> > {
> > std::vector<std::function<void(T)>> _conts;
> > template<class U> auto then(U &&c)
> > {
> > typedef decltype(c(std::declval<T>())) result_type;
> > promise<result_type> p;
> > future<result_type> ret(p.get_future());
> > // Unfortunately libstdc++'s std::function currently always tries to
> > copy lambda types
> > //_conts.emplace_back([p=std::move(p), c=std::forward<U>(c)](T
> > v){p.set_value(c(v));});
> > _conts.push_back([p =
> > std::make_shared<promise<result_type>>(std::move(p)),
> > c=std::forward<U>(c)](T v){p->set_value(c(v));});
> > return ret;
> > }
> > BOOST_CONSTEXPR T operator()(T v) const
> > {
> > for(auto &i : _conts)
> > i(v);
> > return v;
> > }
> > };
> >
> > All this continuation does is provide a .then(callable) which stores those
> > callables into a std::vector and its operator() executes the previously
> > stored callables.
>
> Previously you defined callables as composed functions, but here you
> execute them sequentially. That is, then(f1).then(f2) gets executed as
> f1(v),f2(v) instead of f2(f1(v)). Or am I missing something?

No, you have a keen eye, that's all.

This relates to a very interesting part of the Concurrency TS in how
their continuations are destructive for future but non-destructive
for shared_future. This code shows what I mean:

future<int> f1(p.get_future());
future<int> f2(f1.then([](future<int>){...});
f1.get(); // throws an exception!

Because futures cannot be copied, and because the callable called by
then must take the incoming future by value, this implies the
destruction of state of the future f1 by its then().

Which is all well and good. But if we were writing generic code, and
we had typedefed our own future alias type, and we decided it
suddenly needs to be a shared_future instead:

shared_future<int> f1(p.get_future());
shared_future<int> f2(f1.then([](shared_future<int>){...});
f1.get(); // works as expected

I have a problem with these semantics. My problem is this: there is a
BIG difference between a future whose lifetime is managed by multiple
holders and a future with atomic consumption (i.e. self destructing)
value semantics. The former is really "go wrap it with shared_ptr",
and I think if you want multiple owners to manage lifetime of
something then instead of reinventing shared_ptr, just use shared_ptr
instead. The latter is rather like an atomic-compare-and-exchange
operation, the very first person who reaches the pie gets all of it,
no exceptions. Where my issue is is that I see no reason why the two
semantics needs to be forced together like that - in particular, an
atomic consuming shared future is obviously useful (though in
fairness can be easily emulated with a shared_ptr to a future), while
a value transporting but move constructing only future has the same
usefulness as unique_ptr or optional future semantics (though in
fairness could be easily emulated with a unique_ptr or optional to a
shared_future).

Besides, thanks to my constexpr reduction I can be much more flexible
with which types of continuation .then() can invoke. I can provide
value consuming ones, rvalue and lvalue ref consuming ones, and whole
future consuming ones. Each is stored in its own std::vector
container with the appropriate std::function type - which, if never
used, gets constexpr out of existence, so you only pay for what you
use.

Therefore, I would hope that in my reimplementation of future
promise, this would still fail as per Concurrency TS:

future<int> f1(p.get_future());
future<int> f2(f1.then([](future<int>){...});
f1.get(); // throws an exception!

But this works:

future<int> f1(p.get_future());
future<int> f2(f1.then([](future<int> &){...});
f1.get(); // works

So does this:

future<int> f1(p.get_future());
future<int> f2(f1.then([](future<int> &&){...});
f1.get(); // depends if the continuation move constructed

And this:

future<int> f1(p.get_future());
future<int> f2(f1.then([](int){...});
f1.get(); // works

And of course this:

future<int> f1(p.get_future());
future<int> f2(f1.then([](expected<int> &){...});
f1.get(); // works

This is because the std::future replica uses a
std::experimental::expected<int, exception_ptr> as the transport
type, so if you wish access to that you can get it.

Niall

-- 
ned Productions Limited Consulting
http://www.nedproductions.biz/ 
http://ie.linkedin.com/in/nialldouglas/



Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk