P0534: call/cc, type safety and exceptions

274 views
Skip to first unread message

Oleg Ageev

unread,
Feb 28, 2017, 9:24:38 PM2/28/17
to ISO C++ Standard - Future Proposals
Hello,

I have some questions about call/cc in proposed implementation

I think possibility put any arguments in continuation and access to them through template function it is lack of type safety, and isn't good idea to propose it in standard.
Why not specialize continuation with type of input and type of output?
And forward arguments of operator() to constructor of input type.
I rewrote continuation based on boost.context implementation.
Also I get rid of data_available/get_data... why not use optional?
What do you think about it?

https://gist.github.com/cNoNim/bb24a613406de30d2c856bc7df1ab424

Also I don't understand why:
If an uncaught exception escapes from the entry-function, std::terminate is called.

We can catch exception in `run` function of `record` and rethrow in `context_exit`

https://gist.github.com/cNoNim/933978b23ccc13746ffe4f21de187ecd

Sorry for custom naming convention.

Nicol Bolas

unread,
Feb 28, 2017, 10:01:25 PM2/28/17
to ISO C++ Standard - Future Proposals


On Tuesday, February 28, 2017 at 9:24:38 PM UTC-5, Oleg Ageev wrote:
Hello,

I have some questions about call/cc in proposed implementation

I think possibility put any arguments in continuation and access to them through template function it is lack of type safety, and isn't good idea to propose it in standard.
Why not specialize continuation with type of input and type of output?

Because:

1) That would limit what you can do with continuations. As currently proposed, you can pass different parameters to different parts of continuations. Your idea fixes the interface at call/cc time.

2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.

And forward arguments of operator() to constructor of input type.
I rewrote continuation based on boost.context implementation.
Also I get rid of data_available/get_data... why not use optional?

Use `optional` to do what? To take up more space? To force me to copy values when I don't have to?

What do you think about it?

https://gist.github.com/cNoNim/bb24a613406de30d2c856bc7df1ab424

Also I don't understand why:
If an uncaught exception escapes from the entry-function, std::terminate is called.

We can catch exception in `run` function of `record` and rethrow in `context_exit`

Again, this would make P0534 higher level than strictly needed. If you want to provide such wrappers, that's great (and welcome). But the low-level tool shouldn't have those.

As I have argued in other threads, I think it is vital to have both low-level tools and high-level tools available. For those who need the greater control, they can have that. But we should also ship with safer mechanisms that handle such things adequately. And standardizing them means that they can be inter-operable.

Oliver Kowalke

unread,
Mar 1, 2017, 2:26:57 AM3/1/17
to std-pr...@isocpp.org
2017-03-01 4:01 GMT+01:00 Nicol Bolas <jmckesson@gmail.com>:


On Tuesday, February 28, 2017 at 9:24:38 PM UTC-5, Oleg Ageev wrote:
Hello,

I have some questions about call/cc in proposed implementation

I think possibility put any arguments in continuation and access to them through template function it is lack of type safety, and isn't good idea to propose it in standard.
Why not specialize continuation with type of input and type of output?

Because:

1) That would limit what you can do with continuations. As currently proposed, you can pass different parameters to different parts of continuations. Your idea fixes the interface at call/cc time.

For instance during the implementation of my coroutine and fiber libraries I encountered that initialization- and termination-phases were required. In those phases no data at all or data of different types are transferred.
Imagine the type of the transferred data does not have an default-ctor - you have to pass an value each time the continuation is resumed. What if you can't produce one or you want to terminate the continuation.
Of course you could use something like variant or optional, but you would pay each time for the overhead even in cases were you don't need variant/optional etc.

I've had several implementations of call/cc - the one  presented in P0534 seams to me the most suitable.

 
2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.

right , that's one of the ideas behind P0534


> We can catch exception in `run` function of `record` and rethrow in `context_exit`

The switch mechanism proposed in P0534 is symmetric, e.g you have to explicitly specify the continuation that has to resumed next.
To which continuation do you want to jump to in context_exit?

Victor Dyachenko

unread,
Mar 1, 2017, 2:48:36 AM3/1/17
to ISO C++ Standard - Future Proposals
Oliver, why not standardize the lowest level part first - fcontext_t et a + stacks allocation? I think this API can be used even with pure C. And we can build anything ontop. call/cc can have some unwanted overhead. I'm sure that fcontext_t-related functions are the main values of Boost.Context library. People still use it even in spite of it were moved into detail. E.g. see https://github.com/facebook/folly/blob/master/folly/fibers/BoostContextCompatibility.h.

Oliver Kowalke

unread,
Mar 1, 2017, 3:26:22 AM3/1/17
to std-pr...@isocpp.org
2017-03-01 8:48 GMT+01:00 Victor Dyachenko <victor.d...@gmail.com>:
I think this API can be used even with pure C.

because it's proposed for C++ - class std::continuation manages the execution context (the stack; one shot-continuation).

 
And we can build anything ontop. call/cc can have some unwanted overhead. I'm sure that fcontext_t-related functions are the main values of Boost.Context library.

callcc() and continuation::operator() are only thin wrapper (only one- or two-lines of code)

Victor Dyachenko

unread,
Mar 1, 2017, 3:45:41 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 11:26:22 AM UTC+3, Oliver Kowalke wrote:

2017-03-01 8:48 GMT+01:00 Victor Dyachenko <victor.d...@gmail.com>:
I think this API can be used even with pure C.

because it's proposed for C++ - class std::continuation manages the execution context (the stack; one shot-continuation).


Sure, but I think I would be good for C/C++ community to have common tools, at least on ABI level. Like atomics or complex numbers.
 
 
And we can build anything ontop. call/cc can have some unwanted overhead. I'm sure that fcontext_t-related functions are the main values of Boost.Context library.

callcc() and continuation::operator() are only thin wrapper (only one- or two-lines of code)

Yes, it is slightly thinner than execution_context v2. But it still controls the lifetime - I need to keep the continuation object somewhere. May be add something like thread::detach() ? 
Message has been deleted

Oleg Ageev

unread,
Mar 1, 2017, 5:40:31 AM3/1/17
to ISO C++ Standard - Future Proposals


On Wednesday, March 1, 2017 at 6:01:25 AM UTC+3, Nicol Bolas wrote:
1) That would limit what you can do with continuations. As currently proposed, you can pass different parameters to different parts of continuations. Your idea fixes the interface at call/cc time.
You also can pass different parameters if use optional/varian/custom type with several different constructors, but get rid of type safety it's very bad idea...
2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.
low level API it's fcontext_t and several functions to context switching, continuation it's high level wrapper
 
Use `optional` to do what? To take up more space? To force me to copy values when I don't have to?
I think even use of `optional` not necessary, e.g. we can throw kind of bad access exception


On Wednesday, March 1, 2017 at 10:26:57 AM UTC+3, Oliver Kowalke wrote:
For instance during the implementation of my coroutine and fiber libraries I encountered that initialization- and termination-phases were required. In those phases no data at all or data of different types are transferred.
Imagine the type of the transferred data does not have an default-ctor - you have to pass an value each time the continuation is resumed. What if you can't produce one or you want to terminate the continuation.
Of course you could use something like variant or optional, but you would pay each time for the overhead even in cases were you don't need variant/optional etc.
also you can use custom type for wrap it and reduce overhead, or hide internals from user

The switch mechanism proposed in P0534 is symmetric, e.g you have to explicitly specify the continuation that has to resumed next.
To which continuation do you want to jump to in context_exit?
If I get you right `context_exit` executed ontop of caller context because in `context_entry` we have:
ontop_fcontext( t.fctx, rec, context_exit< Rec >);
and if we catch exception in continuation, we can rethrow it to caller context in context_exit
or I don't understand some use cases?

Oliver Kowalke

unread,
Mar 1, 2017, 5:59:24 AM3/1/17
to std-pr...@isocpp.org
2017-03-01 11:39 GMT+01:00 <og...@agnicore.com>:
2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.

low level API it's fcontext_t and several functions to context switching, continuation it's high level wrapper

that's wrong
 
On Wednesday, March 1, 2017 at 10:26:57 AM UTC+3, Oliver Kowalke wrote:
For instance during the implementation of my coroutine and fiber libraries I encountered that initialization- and termination-phases were required. In those phases no data at all or data of different types are transferred.
Imagine the type of the transferred data does not have an default-ctor - you have to pass an value each time the continuation is resumed. What if you can't produce one or you want to terminate the continuation.
Of course you could use something like variant or optional, but you would pay each time for the overhead even in cases were you don't need variant/optional etc.
also you can use custom type for wrap it and reduce overhead, or hide internals from use

you still pay for thinks you don't need


The switch mechanism proposed in P0534 is symmetric, e.g you have to explicitly specify the continuation that has to resumed next.
To which continuation do you want to jump to in context_exit?
If I get you right `context_exit` executed ontop of caller context because in `context_entry` we have:
ontop_fcontext( t.fctx, rec, context_exit< Rec >);
and if we catch exception in continuation, we can rethrow it to caller context in context_exit
or I don't understand some use cases?

if you throw an exception in the middle of your continuation and it it escapes the top-level continuation you have no guaranttee that the calling continuation is still valid (might resumed and termianted already)

Oleg Ageev

unread,
Mar 1, 2017, 6:16:29 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 1:59:24 PM UTC+3, Oliver Kowalke wrote:
if you throw an exception in the middle of your continuation and it it escapes the top-level continuation you have no guaranttee that the calling continuation is still valid (might resumed and termianted already)
I don't understand how calling continuation can be not vaild, if you have
BOOST_ASSERT( nullptr != t.fctx);
before call ontop_fcontext
and if calling continuation existis, and it not top level continuation, when it's also catch exception, and rethrow to caller

Can you mean that the context is lost because it is moved to the functor?
but we can save calling context for use in catch
    Transfer run(Transfer t)
   
{
     
Ctx from{ t };
     
auto recovery = from.t.fctx;
     
try
     
{
       
Ctx cc = std::invoke(f, std::move(from));
       
return { std::exchange(cc.t.fctx, nullptr), nullptr };
     
}
     
catch (ForcedUnwind const&)
     
{ throw; }
     
catch (...)
     
{
       
except = std::current_exception();
       
return { recovery, nullptr };
     
}
   
}
`recovery` it's pointer to calling context

Oliver Kowalke

unread,
Mar 1, 2017, 6:30:54 AM3/1/17
to std-pr...@isocpp.org
you can't use recovery after calling std::invloke() because during the execution of f the context associated with f.ctx/recovery might have been resumed (so recovery points somewhere in the stackframe) or the context was terminated before the exception is thrown out of f.

suppose A calls callccc(f) -> that creates B : t.fctx/recovery points  to the stack of A
B resumes C (created somewhere/before A calls callcc() ...)
C resumes A
A terminates
after A has terminated B is resumed (because A has specified B as the context that has to be resumed after A's termination)
B resumes and throws an exception
exception is catched in run()
returning recovery ends up in UB because the stack space recovery points to is invalidated/deleted or was reused and overwritten ....

Giovanni Piero Deretta

unread,
Mar 1, 2017, 6:39:18 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 3:01:25 AM UTC, Nicol Bolas wrote:

On Tuesday, February 28, 2017 at 9:24:38 PM UTC-5, Oleg Ageev wrote:
Hello,

I have some questions about call/cc in proposed implementation

I think possibility put any arguments in continuation and access to them through template function it is lack of type safety, and isn't good idea to propose it in standard.
Why not specialize continuation with type of input and type of output?

Because:

1) That would limit what you can do with continuations. As currently proposed, you can pass different parameters to different parts of continuations. Your idea fixes the interface at call/cc time.

2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.

FWIW, I strongly disagree. Getting continuation correctly typed is important and any help the compiler can give when using such a mind-bending construct as a continuation is very welcome. In particular getting the typing of something like invoke_on_top is non trivial (it certainly took me a few tries).

The proposal doesn't seem to describe the semantics, but get_data<T> is either unsafe (i.e. the equivalent of a reinterpret_cast) or, if checked, adds non trivial overhead as it requires passing the typeid in addition to the value itself and checking against it.

In the first case you are better off using explicit void* in your continuation type so that the type safety hole is evident; in the second case, the dynamic checking should be opt in, by explicit using std::any, std::variant or something like that, otherwise it would violate the goal of being a zero cost abstraction.

I would like to add that, with the current ephemeral continuation semantics [1] and with the existence of invoke_on_top, the need for a dynamic continuation type is greatly reduced as you can very cheaply capture a new continuation with a new type.

Regarding optionality, unfortunately my experience matches Oliver's and it is pretty much a requirement that any continuation<T> in practice needs to support yielding with no value.

[1] Ephemeral in the sense that a logical continuation exists only between the capture point and its restoration point; the fact that we may (or not) reuse the same std::continuation object to refer to distinct continuations is immaterial and only done for convenience.

Oleg Ageev

unread,
Mar 1, 2017, 6:49:12 AM3/1/17
to ISO C++ Standard - Future Proposals


On Wednesday, March 1, 2017 at 2:30:54 PM UTC+3, Oliver Kowalke wrote:
you can't use recovery after calling std::invloke() because during the execution of f the context associated with f.ctx/recovery might have been resumed (so recovery points somewhere in the stackframe) or the context was terminated before the exception is thrown out of f.
can you please explain problem by code...
I try implement problem but I don't understand how B after resuming can throw exception
B depends on A and if A terminated B should be resumed only for context_unwind by destructor
but ForceUnwind don't catched by run and work correcty...
I found some errors in my version of run now I try use that
    Transfer run(Transfer t)
   
{
     
Ctx from{ t };

     
try
     
{
       
Ctx cc = std::invoke(f, std::move(from));
       
return { std::exchange(cc.t.fctx, nullptr), nullptr };
     
}
     
catch (ForcedUnwind const&)
     
{ throw; }
     
catch (...)
     
{
       
except = std::current_exception();

       
return { std::exchange(from.t.fctx, nullptr), nullptr };
     
}
   
}

and try implement problem like this

  std::exception_ptr eptr;
 
try
 
{
   
auto C = Continuation<void, void>::callcc([](auto && yield)
   
{
     
int counter = 0;
     
do
     
{
       
if (counter == 10)
         
throw std::bad_function_call();
        counter
++;
     
} while (yield = yield());
     
return std::move(yield);
   
});

   
auto B = Continuation<void, int>::callcc([&C](auto && yield)
   
{
     
int ret = 0;
     
do
     
{
       
if (!(C = C())) break;
     
} while (yield = yield(ret++));
     
return std::move(yield);
   
});

   
int ent = 0;
   
while (B = B())
   
{
     
auto ret = B.get();
     
if (ret >= 100) break;
   
}
     
    std
::cout << "main: done" << std::endl;
    std
::cin.ignore();
 
}
 
catch (...)
 
{ eptr = std::current_exception(); }

and all works correctly

Oleg Ageev

unread,
Mar 1, 2017, 6:53:43 AM3/1/17
to ISO C++ Standard - Future Proposals
exception can be thrown by some destructor in context_unwind
but exception in destructor it's bad practice
now I'try implement this

Oleg Ageev

unread,
Mar 1, 2017, 6:59:24 AM3/1/17
to ISO C++ Standard - Future Proposals
yes exception leaked if thrown by destructor
  std::exception_ptr eptr;
 
try
 
{
   
auto C = Continuation<void, void>::callcc([](auto && yield)
   
{
     
int counter = 0;
     
do
     
{

        counter
++;
     
} while (yield = yield());
     
return std::move(yield);
   
});

   
auto B = Continuation<void, int>::callcc([&C](auto && yield)
   
{

     
struct BAD { ~BAD() { throw std::bad_function_call(); } };
      BAD bad
;

     
int ret = 0;
     
do
     
{
       
if (!(C = C())) break;
     
} while (yield = yield(ret++));      
     
return std::move(yield);
   
});

   
int ent = 0;
   
while (B = B())
   
{
     
auto ret = B.get();
     
if (ret >= 100) break;
   
}
     
    std
::cout << "main: done" << std::endl;
 
}

 
catch (...)
 
{
    eptr
= std::current_exception();
 
}

but how we can workaround this if don't support exceptions catching?

Giovanni Piero Deretta

unread,
Mar 1, 2017, 7:03:17 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 11:59:24 AM UTC, Oleg Ageev wrote:
yes exception leaked if thrown by destructor

you have the same issue if the lambda moves the continuation to a local variable, takes it by value instead of reference, or pass it to another function by value, etc....

-- gpd


Oleg Ageev

unread,
Mar 1, 2017, 7:08:57 AM3/1/17
to ISO C++ Standard - Future Proposals


On Wednesday, March 1, 2017 at 3:03:17 PM UTC+3, Giovanni Piero Deretta wrote:
you have the same issue if the lambda moves the continuation to a local variable, takes it by value instead of reference, or pass it to another function by value, etc....
continuation is movable only object, how it can be taked by value?
 

Oleg Ageev

unread,
Mar 1, 2017, 7:13:15 AM3/1/17
to ISO C++ Standard - Future Proposals
and also I got such warning

warning C4297: 'main::<lambda_47e6b9d8c7d076365105bb6276e58c85>::()::BAD::~BAD': function assumed not to throw an exception but does
note
: destructor or deallocator has a (possibly implicit) non-throwing exception specification

destructor should be noexcept, and I see no other way to break exceptions catching

Giovanni Piero Deretta

unread,
Mar 1, 2017, 7:14:59 AM3/1/17
to std-pr...@isocpp.org
One does not exclude the other:
 Continuation x;
[](Continuation c) {....} (std::move(x)); //look ma, no copies!

--
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/F-8aMUBPMzc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/a880928c-208a-4f30-8967-bf29fac87bf2%40isocpp.org.

Oleg Ageev

unread,
Mar 1, 2017, 7:35:04 AM3/1/17
to ISO C++ Standard - Future Proposals


On Wednesday, March 1, 2017 at 3:14:59 PM UTC+3, Giovanni Piero Deretta wrote:

Did you talk about that?
  std::exception_ptr eptr;
 
try
 
{
   
auto C = Continuation<void, void>::callcc([](auto && yield)
   
{
     
int counter = 0;
     
do
     
{

       
if (counter == 10) throw std::bad_function_call();

        counter
++;
     
} while (yield = yield());
     
return std::move(yield);
   
});


   
auto B = Continuation<void, int>::callcc([C=std::move(C)](auto && yield) mutable
   
{

     
int ret = 0;
     
do
     
{
       
if (!(C = C())) break;
     
} while (yield = yield(ret++));      
     
return std::move(yield);
   
});

   
int ent = 0;
   
while (B = B())
   
{
     
auto ret = B.get();
     
if (ret >= 100) break;
   
}
     
    std
::cout << "main: done" << std::endl;
 
}
 
catch (...)
 
{
    eptr
= std::current_exception();
 
}

but in such case exception catched and rethrown correctly

Giovanni Piero Deretta

unread,
Mar 1, 2017, 7:38:31 AM3/1/17
to std-pr...@isocpp.org
s/auto&& yield/auto yield/ and see if things still work properly.

--
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/F-8aMUBPMzc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Oleg Ageev

unread,
Mar 1, 2017, 7:47:05 AM3/1/17
to ISO C++ Standard - Future Proposals
Also... about yield from continuation without value...
I understand it's need to terminate, but we can just return from continuation in any place, and continuation will be terminated, but we don't need any value for that.
What else needed for termination of coroutine?
Also in initialization state we can not call continuation.
I try to write kind of coroutine based on my version of continuations

Oliver Kowalke

unread,
Mar 1, 2017, 7:51:41 AM3/1/17
to std-pr...@isocpp.org
    Transfer run(Transfer t)
   
{
     
Ctx from{ t };
     
auto recovery = from.t.fctx;
     
try
     
{
       
Ctx cc = std::invoke(f, std::move(from));
       
return { std::exchange(cc.t.fctx, nullptr), nullptr };
     
}
     
catch (ForcedUnwind const&)
     
{ throw; }
     
catch (...)
     
{
       
except = std::current_exception();
       
return { recovery, nullptr };
     
}
   
}

no matter what the code will not work - you have no guarantee that 'recovery' is still valid in the catch clause
as I explained the stack 'recovery' is pointing to might be invalidated during execution of 'f'

Nicol Bolas

unread,
Mar 1, 2017, 10:38:13 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 6:39:18 AM UTC-5, Giovanni Piero Deretta wrote:
On Wednesday, March 1, 2017 at 3:01:25 AM UTC, Nicol Bolas wrote:

On Tuesday, February 28, 2017 at 9:24:38 PM UTC-5, Oleg Ageev wrote:
Hello,

I have some questions about call/cc in proposed implementation

I think possibility put any arguments in continuation and access to them through template function it is lack of type safety, and isn't good idea to propose it in standard.
Why not specialize continuation with type of input and type of output?

Because:

1) That would limit what you can do with continuations. As currently proposed, you can pass different parameters to different parts of continuations. Your idea fixes the interface at call/cc time.

2) P0534 is intended to be as low level as possible while still being functional. By specializing types, it would be higher level than strictly necessary.

FWIW, I strongly disagree. Getting continuation correctly typed is important and any help the compiler can give when using such a mind-bending construct as a continuation is very welcome. In particular getting the typing of something like invoke_on_top is non trivial (it certainly took me a few tries).

A type-safe version of the data passing API is important and should be shipped with this system. The reason being that the system can implement it more efficiently than the user can.

For example, the current system could gain reasonable type safety by passing everything via `std::any`. However, doing so brings with it some baggage. `any` can only be used with copyable types, which is an unnecessary restriction for our needs. If the `T` is somewhat large, `any` will dynamically allocate memory for it, whereas the unsafe API always uses the continuation's stack for the object.

Now of course, one can implement safe typing by wrapping `callcc` and `continuation` by implementing the guts of an `any`. Essentially, each numbered parameter would be 2 actual parameters: the value itself and its `type_index`. And when you cast it back, if the index you ask for doesn't match the one used by the parameter, an exception is thrown or whatever.

The problem is, as always with low-level tools, inter-operability. If you don't provide higher-level tools at the same time as lower-level ones, you encourage fragmentation. My `safe_callcc/safe_continuation` can't work with someone else's `safe_callcc/safe_continuation`, even if they're doing the same thing.

So we need the safe mechanism, and we need it shipped with the system. But that should not be the only way; the lower-level form should still exist.

Now, maybe the type-safe version should be the default, so that if you want the lower-level, unsafe API, we can clearly see that safety is being sacrificed. So `callcc/continuation` would naturally be type-safe, with `unsafe_callcc/unsafe_continuation` being not type safe.

Giovanni Piero Deretta

unread,
Mar 1, 2017, 10:48:40 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 3:38:13 PM UTC, Nicol Bolas wrote:
[...]


So we need the safe mechanism, and we need it shipped with the system. But that should not be the only way; the lower-level form should still exist.

Now, maybe the type-safe version should be the default, so that if you want the lower-level, unsafe API, we can clearly see that safety is being sacrificed. So `callcc/continuation` would naturally be type-safe, with `unsafe_callcc/unsafe_continuation` being not type safe.

we are in agreement then, I just want to point out that, given a type safe std::continuation<T> implementation, the unsafe variant would simply be std::continuation<void*>, while the dynamic variant would be continuation<std::any_ptr>, given a suitable implementation of any_ptr.

Oleg Ageev

unread,
Mar 1, 2017, 11:05:56 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 3:51:41 PM UTC+3, Oliver Kowalke wrote:
no matter what the code will not work - you have no guarantee that 'recovery' is still valid in the catch clause
as I explained the stack 'recovery' is pointing to might be invalidated during execution of 'f'
I don't understand, Ok, exception handling will not work in that place, but exception handling implemented in boost.coroutine2, why such mechanism can't be implemented on top of any continuation?

Oliver Kowalke

unread,
Mar 1, 2017, 11:18:02 AM3/1/17
to std-pr...@isocpp.org
bosot.coroutine2 is based on call/cc - but in contrast to call/cc the coroutiens provided by bosot.coroutine2 are asymmetric
asymmetric means that caller and callee are tighly coupled -> caller can only jump to callee and the callee can only jump to its caller
call/cc from P0534 is symmetric - that enables asymmetric coroutines (as boost.coroutine2 provides) as well someting like fibers (boost.fiber is based on callcc too). fibers are not stronly coupled (a fiber can switch to any other abritrary fiber)

Oliver Kowalke

unread,
Mar 1, 2017, 11:26:16 AM3/1/17
to std-pr...@isocpp.org
2017-03-01 16:48 GMT+01:00 Giovanni Piero Deretta <gpderetta@gmail.com>:
On Wednesday, March 1, 2017 at 3:38:13 PM UTC, Nicol Bolas wrote:
[...]

So we need the safe mechanism, and we need it shipped with the system. But that should not be the only way; the lower-level form should still exist.

Now, maybe the type-safe version should be the default, so that if you want the lower-level, unsafe API, we can clearly see that safety is being sacrificed. So `callcc/continuation` would naturally be type-safe, with `unsafe_callcc/unsafe_continuation` being not type safe.

we are in agreement then, I just want to point out that, given a type safe std::continuation<T> implementation, the unsafe variant would simply be std::continuation<void*>

and this would require std::continuation<void> too - so we end up in three specialisations of std::continuation<>

I prefer a template-less std::continuation - it's more flexible.
For instance the implementation of P0534 (continuation not a template)  is used to implement boost.coroutine2 (higher-level abstraction) which is type-safe.

Giovanni Piero Deretta

unread,
Mar 1, 2017, 11:40:20 AM3/1/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 4:26:16 PM UTC, Oliver Kowalke wrote:
2017-03-01 16:48 GMT+01:00 Giovanni Piero Deretta <gpderetta@gmail.com>:
On Wednesday, March 1, 2017 at 3:38:13 PM UTC, Nicol Bolas wrote:
[...]

So we need the safe mechanism, and we need it shipped with the system. But that should not be the only way; the lower-level form should still exist.

Now, maybe the type-safe version should be the default, so that if you want the lower-level, unsafe API, we can clearly see that safety is being sacrificed. So `callcc/continuation` would naturally be type-safe, with `unsafe_callcc/unsafe_continuation` being not type safe.

we are in agreement then, I just want to point out that, given a type safe std::continuation<T> implementation, the unsafe variant would simply be std::continuation<void*>

and this would require std::continuation<void> too - so we end up in three specialisations of std::continuation<>


My preference for a low level design would be a std::countinuation<T1,T2> (the in and out parameters need not be the same), that only allows forwarding pointers to T1, T2 (that what continuation would do internally anyway). The empty case is handled simply by passing nullptr. There is no need for any specialization this way.

 
I prefer a template-less std::continuation - it's more flexible.

If you want to provide a template less std::continuation, then you should drop the typed get_data<T> and simply have a void* get_data().


Nicol Bolas

unread,
Mar 1, 2017, 11:50:46 AM3/1/17
to ISO C++ Standard - Future Proposals

I don't think you understood what I meant when I said "type safe". I specifically don't mean that the continuation has a static and typed set of parameters. What I mean is that getting the parameter back is type-checked and therefore safe. But it's a runtime check, not a compile-time check.

This prevents compile-time tight coupling while still having safety on the receiving end.

Giovanni Piero Deretta

unread,
Mar 1, 2017, 12:18:07 PM3/1/17
to ISO C++ Standard - Future Proposals

Hum, but this means that those that want statically check type safety (a very common use case, think generators), are left in the cold and need to roll their own continuation type on top of the unsafe one. I still don't see the need for a dynamically checked continuation type.

For example, std::function is a vocabulary type designed for interoperability and decoupling, but it still check the signature a compile time. A runtime checked std::function is possible, but not desirable (incidentally Qt slot types used to be only dynamically checked and it was a pain). I don't see why continuation would be different (in fact there is a very strong affinity between continuations and functions).
 

Nicol Bolas

unread,
Mar 1, 2017, 3:22:01 PM3/1/17
to ISO C++ Standard - Future Proposals

Static checking between the parameters to `callcc` and the parameters to the function being invoked is one thing. But static checking between the parameters to `continuation::operator()` and to the receiving code is quite something else. It is perfectly reasonable to initiate a `callcc` with one set of parameters, but provide different parameters of different types at continuation time.

Also, it's important to recognize that the function which began the `callcc` operation may be doing generic boilerplate stuff: storing a promise, building a channel, etc. Its return value will in many cases be irrelevant. So it doesn't make sense to have `callcc` statically assume that the return value of that function is meaningful to the caller.

The fundamental difference between `std::function` and a continuation is that the former is intended to be a go-between and the latter is not. The former is just a value type for storing and invoking any C++ callable. `callcc` has nothing to do with that; it's creating new functionality. And that new functionality has its own needs.

For example, guaranteed elision should still work through a `std::function` call, for both parameters and return values. But there's no way to make it work through a continuation; there has to be a copy/move of parameters into the continuation's stack, and a copy/move of return values from the continuation's stack.

So clearly, we're talking about two very different things here.

Bengt Gustafsson

unread,
Mar 2, 2017, 4:43:30 AM3/2/17
to ISO C++ Standard - Future Proposals
Hi.

I do really like the general idea of callcc and continuation class. But I don't understand the need of data transfer at this low abstraction level. After all, these are not threads, so there is no asynchronism to think about. To prove this point I implemented the classical generator pattern without using continuation's data transfer possibilities (see attached file). Sure enough, when the generator lambda calls yield(value) it stops processing until the main program calls operator++ again. So all this time the parameter to yield is valid on the generator's stack and available by reference to the main program. So while there are obviously needs to send data both ways for different patterns I fail to see why it must be a feature of the lowest level abstraction.

When I wrote the code for generator.h I also saw that the pattern `mContinuation = mContinuation()` seems to be mandatory. If you forget the assignment you loose the ability to continue the next time. While there may be cases where you want to assign the continuation returned from operator() to some other std::continuation object, wouldn't it be more appropriate to make simple things simple by letting operator() return void and automatically update its `this` to the new state before returning and then applications which really do need to move the continuation object elsewhere can do so afterwards. Again there are no threading issues so it should be safe. In the back of my head I think there is some exception safety issue at play here, but if so it needs to be explained more clearly.

A comment on the invoke_on_top_t functionality. Why add a special tag type just to be able to overload operator() to do another task when there is the possibility to add a regular method: std::continuation::invoke_on_top(args...). Also it would be interesting to know when this rather strange functionality is useful.

An issue I stumbled upon during implementation was not being able to do callcc() on a method. I think an overload of callcc specifically for method pointers (in parallel with the std::function constructor set) will be needed. This is as the std::continuation&& parameter gets prepended to the args of callcc when calling the provided function. If the provided function is a std::function wrapping a method pointer its operator() will expect to get the 'this' of the object to call and instead it will get the std::continuation prepended by callcc!

// Example:

class MyClass {
    void fun(std::continuation&& cont);
};

MyClass myObj;

callcc(&MyClass::fun, myObj);    // This is what you want to write, but callcc does not take method pointers.

// So you may try this:

callcc(std::function<void(MyClass&, std::continuation&&)>(&MyClass::fun), myObj);   // Mismatched types as callcc gets the order wrong.

// Instead you have to conjure up a lambda:

callcc([&](std::continuation&& cont) { myObj.fun(std::move(cont); });


While the lambda solution works I think it is quite weak not to allow the first version.


generator.h

Giovanni Piero Deretta

unread,
Mar 2, 2017, 5:11:08 AM3/2/17
to ISO C++ Standard - Future Proposals
On Thursday, March 2, 2017 at 9:43:30 AM UTC, Bengt Gustafsson wrote:
Hi.

I do really like the general idea of callcc and continuation class. But I don't understand the need of data transfer at this low abstraction level.

You can of course arrange to have both sides of the continuation share a memory location, but it is cumbersome (especially when the two sides can switch freely like with symmetric coroutines) and potentially inefficient (The parameter passing can be made literally free in the low level assembler switching function by simply not saving and restoring a specific register). The implementation will need to pass parameters through anyway (to implement execute_on_top or exception transport, so it makes sense to expose the functionality to the user. Although I would prefer the functionality to be properly typed, I would settle for a single void* parameter as typing can be implemented on top of it with no cost.

 
After all, these are not threads, so there is no asynchronism to think about. To prove this point I implemented the classical generator pattern without using continuation's data transfer possibilities (see attached file). Sure enough, when the generator lambda calls yield(value) it stops processing until the main program calls operator++ again. So all this time the parameter to yield is valid on the generator's stack and available by reference to the main program. So while there are obviously needs to send data both ways for different patterns I fail to see why it must be a feature of the lowest level abstraction.

well, yes the functionality is normally implemented by simply passing to the next continuation a pointer to the parameter on the stack.
 

When I wrote the code for generator.h I also saw that the pattern `mContinuation = mContinuation()` seems to be mandatory. If you forget the assignment you loose the ability to continue the next time. While there may be cases where you want to assign the continuation returned from operator() to some other std::continuation object, wouldn't it be more appropriate to make simple things simple by letting operator() return void and automatically update its `this` to the new state before returning and then applications which really do need to move the continuation object elsewhere can do so afterwards. Again there are no threading issues so it should be safe. In the back of my head I think there is some exception safety issue at play here, but if so it needs to be explained more clearly.

My own implementation updates this, but I don't have a strong opinion, and the explicit assignment makes it more obvious that the new continuation has potentially no relation to the one just consumed.
 

A comment on the invoke_on_top_t functionality. Why add a special tag type just to be able to overload operator() to do another task when there is the possibility to add a regular method: std::continuation::invoke_on_top(args...).

A possibility is dropping operator() and using callcc for everything. The full callcc signature woud be like this:

   callcc(cont, fn, arg)

Which is the same as cont(invoke_on_top, fn, args)

A new continuation can be created with:

  callcc(make_empty_continuation(), fn, arg)

(where make_empty_continuation just creates a continuation that will exit immediately upon invocation)
Switching is just:

  callcc(cont, identity, arg);

This closely mimics callcc behaviour in other languages. In fact make_empty_continuation() and callcc are really the only two primitives you need.

Of course overloads and default parameters would make use of the library more straightforward (and, in the case of context switching, more efficient).

Also it would be interesting to know when this rather strange functionality is useful.

The functionality is actually not strange at all if you think of the underlying CPS transformation, and it simplifies the implementation of continuation itself, as it can be directly used to implement continuation creation (just invoke on top of an empty continuation), exit (invoke on top of next and cleanup source), exception throwing (invoke on top then throw). It also greatly simplifies more complex uses of continuation (handy to implement scheduling, waiting, etc).
 
An issue I stumbled upon during implementation was not being able to do callcc() on a method. I think an overload of callcc specifically for method pointers (in parallel with the std::function constructor set) will be needed. This is as the std::continuation&& parameter gets prepended to the args of callcc when calling the provided function. If the provided function is a std::function wrapping a method pointer its operator() will expect to get the 'this' of the object to call and instead it will get the std::continuation prepended by callcc!

// Example:

class MyClass {
    void fun(std::continuation&& cont);
};

MyClass myObj;

callcc(&MyClass::fun, myObj);    // This is what you want to write, but callcc does not take method pointers.

// So you may try this:

callcc(std::function<void(MyClass&, std::continuation&&)>(&MyClass::fun), myObj);   // Mismatched types as callcc gets the order wrong.

// Instead you have to conjure up a lambda:

callcc([&](std::continuation&& cont) { myObj.fun(std::move(cont); });


While the lambda solution works I think it is quite weak not to allow the first version.

I think a fully fledged implementation would have full INVOKE like semantics, but as it is very straightforward, probably implementing the feature is not very high in Oliver's priorities.
 

Giovanni Piero Deretta

unread,
Mar 2, 2017, 5:52:43 AM3/2/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 1, 2017 at 8:22:01 PM UTC, Nicol Bolas wrote:
[...]
 
Static checking between the parameters to `callcc` and the parameters to the function being invoked is one thing. But static checking between the parameters to `continuation::operator()` and to the receiving code is quite something else. It is perfectly reasonable to initiate a `callcc` with one set of parameters, but provide different parameters of different types at continuation time.
 

Also, it's important to recognize that the function which began the `callcc` operation may be doing generic boilerplate stuff: storing a promise, building a channel, etc. Its return value will in many cases be irrelevant. So it doesn't make sense to have `callcc` statically assume that the return value of that function is meaningful to the caller.

You are taking my parallel to std::funciton too literally.  There is no expectation that the parameters passed to the function object on context creation to match the continuation return and receive type (and the function object must always return a continuation, so there is no freedom there). My request is that the continuation encodes the types of the parameter passed through and received from and enforce it statically, the same way that std::function enforces them.
 

The fundamental difference between `std::function` and a continuation is that the former is intended to be a go-between and the latter is not.

It very much is. it is a communication channel between two arbitrary thread of executions. In fact delimited continuations [1] are really indistinguishable from functions from the outside, but that's beside the point.
 
The former is just a value type for storing and invoking any C++ callable. `callcc` has nothing to do with that; it's creating new functionality. And that new functionality has its own needs.

For example, guaranteed elision should still work through a `std::function` call, for both parameters and return values. But there's no way to make it work through a continuation; there has to be a copy/move of parameters into the continuation's stack, and a copy/move of return values from the continuation's stack.

Apart from the fact that cross coroutine parameter copy elision is very much feasible with compiler help, you are taking the parallel too literally.

[1] contrary to what P0534 claims, it describes delimited continuations; I'm not even sure you can have undelimited continuations when you have one-shot semantics.

Oliver Kowalke

unread,
Mar 2, 2017, 6:09:49 AM3/2/17
to std-pr...@isocpp.org
2017-03-02 11:52 GMT+01:00 Giovanni Piero Deretta <gpde...@gmail.com>:

[1] contrary to what P0534 claims, it describes delimited continuations; I'm not even sure you can have undelimited continuations when you have one-shot semantics.

undelimited continuations == no delimiter that tells you where the continuatio nends, e.g. if you call callcc() from main(), it is undelimited

int main() {
   ....
   callcc(); // reifies continuation
   ... // all the code behind callcc() belong s to the reified continuation
}


delimited continuations == you doe definie in the code till which point the reified continuation goes (reset-op),

int main() {
    ...
     reset { // delimites the continuation
         shift {...}   / reifies the continuation
     }  // continuation ends here
   ... // code here does not belong to the reified continuation
}

Giovanni Piero Deretta

unread,
Mar 2, 2017, 6:29:35 AM3/2/17
to ISO C++ Standard - Future Proposals
On Thursday, March 2, 2017 at 11:09:49 AM UTC, Oliver Kowalke wrote:
2017-03-02 11:52 GMT+01:00 Giovanni Piero Deretta <gpde...@gmail.com>:

[1] contrary to what P0534 claims, it describes delimited continuations; I'm not even sure you can have undelimited continuations when you have one-shot semantics.

undelimited continuations == no delimiter that tells you where the continuatio nends, e.g. if you call callcc() from main(), it is undelimited

there are a whole family of delimited continuations semantics each with slightly different delimiter/invoker semantics. shift/reset is just one of them.

With undelimted continuation you can return from call/cc and fall back in the original continuation, while still holding the captured continuation, allowing returning 'twice'. This is of course not possible with oneshot continuation, and callcc (and yield), in a way, do inserts a prompt. Now, if I understand correctly, with shift/reset, shift finds the closest dynamically scoped reset in the callstack, while with callcc and yield, there is not a clear distinction between the prompt and the shift point as they are both continuations and they are statically scoped (on the other hand, asymmetric coroutines with an explicit thread local parent pointer more closely match shift/reset model).

Regarding main, the continuation is implicitly delimited as you can't fall further than that and return to the os instead. Oleg Kiselyov (which has done a lot of work on the topic) in fact claims that undelimited continuations do not really exist, as a delimiter always exists, it might just be outside of the programmer control.

Anyway, I don't pretend to understand fully the formal semantics of continuations, so I might be mistaken. It doesn't really matter that much for us C++ programmers though .

-- gpd

Giovanni Piero Deretta

unread,
Mar 2, 2017, 6:31:17 AM3/2/17
to ISO C++ Standard - Future Proposals
On Thursday, March 2, 2017 at 11:29:35 AM UTC, Giovanni Piero Deretta wrote:
[...]

Now, if I understand correctly, with shift/reset, shift finds the closest dynamically scoped reset in the callstack, while with callcc and yield, there is not a clear distinction between the prompt and the shift point as they are both continuations and they are statically scoped (on the other hand, asymmetric coroutines with an explicit thread local parent pointer more closely match shift/reset model).

I meant lexically scoped, not statically scoped here, of course.

-- gpd

Oliver Kowalke

unread,
Mar 2, 2017, 7:35:20 AM3/2/17
to std-pr...@isocpp.org
2017-03-02 10:43 GMT+01:00 Bengt Gustafsson <bengt.gustafsson@beamways.com>:
But I don't understand the need of data transfer at this low abstraction level. After all, these are not threads, so there is no asynchronism to think about.

P0534 is symmetric context switching, e.g. no coupling between caller and callee as it is for generators/coroutines

 
When I wrote the code for generator.h I also saw that the pattern `mContinuation = mContinuation()` seems to be mandatory. If you forget the assignment you loose the ability to continue the next time. While there may be cases where you want to assign the continuation returned from operator() to some other std::continuation object, wouldn't it be more appropriate to make simple things simple by letting operator() return void and automatically update its `this` to the new state before returning and then applications which really do need to move the continuation object elsewhere can do so afterwards.

I've had several variations of callcc() implemented and I got into troubles during the implementation of fibers (lightweight threads) using such a kind of call/cc.
I would not consider to change the identity of a continuation while invoking its operator() a good idea - you don't know to which context your continuation points to (e.g. what it executes/does) after returning from continuation::operator().

 
A comment on the invoke_on_top_t functionality. Why add a special tag type just to be able to overload operator() to do another task when there is the possibility to add a regular method: std::continuation::invoke_on_top(args...).

both is possible : because apply-operator does the context switch I'd like to use the same operator to do the context switch + invoking an function on the resumed context

 
Also it would be interesting to know when this rather strange functionality is useful.

described in P0534:
you can invoke a new function after resuming the context that throws an exception
for instance you want abort and unwind the stack - instead to check for abort each time the context is resumed you call invoke-ontop functionality to execute the throwing abort function only at the time point you know you want to abort the continuation/context

 
An issue I stumbled upon during implementation was not being able to do callcc() on a method. I think an overload of callcc specifically for method pointers (in parallel with the std::function constructor set) will be needed. This is as the std::continuation&& parameter gets prepended to the args of callcc when calling the provided function. If the provided function is a std::function wrapping a method pointer its operator() will expect to get the 'this' of the object to call and instead it will get the std::continuation prepended by callcc!

// Example:

class MyClass {
    void fun(std::continuation&& cont);
};

MyClass myObj;

callcc(&MyClass::fun, myObj);    // This is what you want to write, but callcc does not take method pointers.

// So you may try this:

callcc(std::function<void(MyClass&, std::continuation&&)>(&MyClass::fun), myObj);   // Mismatched types as callcc gets the order wrong.

// Instead you have to conjure up a lambda:

callcc([&](std::continuation&& cont) { myObj.fun(std::move(cont); });


While the lambda solution works I think it is quite weak not to allow the first version.

I'm too busy - so it didn't got the highest prio/keep in mind I do it at my freetime

Oliver Kowalke

unread,
Mar 2, 2017, 11:08:54 AM3/2/17
to std-pr...@isocpp.org


2017-03-02 13:29 GMT+01:00 Oliver Kowalke <oliver....@gmail.com>:
 
An issue I stumbled upon during implementation was not being able to do callcc() on a method. I think an overload of callcc specifically for method pointers (in parallel with the std::function constructor set) will be needed. This is as the std::continuation&& parameter gets prepended to the args of callcc when calling the provided function. If the provided function is a std::function wrapping a method pointer its operator() will expect to get the 'this' of the object to call and instead it will get the std::continuation prepended by callcc!

// Example:

class MyClass {
    void fun(std::continuation&& cont);
};

MyClass myObj;

callcc(&MyClass::fun, myObj);    // This is what you want to write, but callcc does not take method pointers.

// So you may try this:

callcc(std::function<void(MyClass&, std::continuation&&)>(&MyClass::fun), myObj);   // Mismatched types as callcc gets the order wrong.

// Instead you have to conjure up a lambda:

callcc([&](std::continuation&& cont) { myObj.fun(std::move(cont); });


While the lambda solution works I think it is quite weak not to allow the first version.

I'm too busy - so it didn't got the highest prio/keep in mind I do it at my freetime

I took a look into the implementation at boost.context - actually it is already supported:

template< typename Fn, typename ... Arg >                                                                                                                     
continuation callcc( Fn && fn, Arg ... arg);

Seams I was too busy and I forgot it to update the proposal.

Bryce Glover

unread,
Mar 2, 2017, 5:21:16 PM3/2/17
to Bengt Gustafsson, std-pr...@isocpp.org
On Thursday, March 2, 2017 at 4:43:30 AM UTC-5, Bengt Gustafsson wrote:


A comment on the invoke_on_top_t functionality. Why add a special tag type just to be able to overload operator() to do another task when there is the possibility to add a regular method: std::continuation::invoke_on_top(args...). Also it would be interesting to know when this rather strange functionality is useful.


     Just a small thought that I had while keeping tabs on this thread, even if only as a mailing-list lurker (for the most part, anyway;) but one use case of something like `invoke_on_top_t` would be to implement a tail-recursive 'trampoline' function.  

— Bryce Glover

Bengt Gustafsson

unread,
Mar 4, 2017, 9:27:45 AM3/4/17
to ISO C++ Standard - Future Proposals

But I don't understand the need of data transfer at this low abstraction level. After all, these are not threads, so there is no asynchronism to think about.

P0534 is symmetric context switching, e.g. no coupling between caller and callee as it is for generators/coroutines.
Exactly, that's why I asked why data transfer is needed _on the call/cc level_. I showed in my example that you can do data transfer on top of this (at the generator level) with essentially no overhead. Giovanni pointed out that there may be the overhead of one register save/restore, and if this the performance gain you are after I think the price of a scary "void*" style interface (hidden behind an unsafe cast) is not worth it, and if the access is to be tested the performance cost is definitely more than a register save.
 


 
When I wrote the code for generator.h I also saw that the pattern `mContinuation = mContinuation()` seems to be mandatory. If you forget the assignment you loose the ability to continue the next time. While there may be cases where you want to assign the continuation returned from operator() to some other std::continuation object, wouldn't it be more appropriate to make simple things simple by letting operator() return void and automatically update its `this` to the new state before returning and then applications which really do need to move the continuation object elsewhere can do so afterwards.

I've had several variations of callcc() implemented and I got into troubles during the implementation of fibers (lightweight threads) using such a kind of call/cc.
I would not consider to change the identity of a continuation while invoking its operator() a good idea - you don't know to which context your continuation points to (e.g. what it executes/does) after returning from continuation::operator().
And you don't know the identity of the continuation returned from operator() in your case, what is the difference? The advantage of the in situ version is however obvious: You can't forget doing the assignment, and thereby leave the continuation object stale. 

 
A comment on the invoke_on_top_t functionality. Why add a special tag type just to be able to overload operator() to do another task when there is the possibility to add a regular method: std::continuation::invoke_on_top(args...).

both is possible : because apply-operator does the context switch I'd like to use the same operator to do the context switch + invoking an function on the resumed context

I find this a very weak reason to go through the hoops of creating and using a specific tag class. 

 
Also it would be interesting to know when this rather strange functionality is useful.

described in P0534:
you can invoke a new function after resuming the context that throws an exception
 
It is a bit hard to understsand this sentense. Do you mean to resume a context after it has thrown an exception? If this is what you mean, there could be two cases: a) the exception was not caught. In this case the stack is completely unwound as if the function had returned, and what you are doing is only to reuse the stack memory, right? b) the exception was caught somewhere during unwindining and the continuation was invoked in a catch clause. Now the invoke_on_top injects some code to be run inside the catch. I still don't see what good this would do. Also, if there is a throw; below the continuation invokation things would get messy if the injected code throws (even if this exception is caught).
 
for instance you want abort and unwind the stack - instead to check for abort each time the context is resumed you call invoke-ontop functionality to execute the throwing abort function only at the time point you know you want to abort the continuation/context
I think you may be saying that once in a while one party injects code that throws if an abort flag is set into another piece of code on the other side of the continuation. Or maybe you test the abort flag first and then injects a function which just throws an exception. Well that would be a way to get rid of the continuation and properly unwind its stack, but didn't you have a specific way of doing that, such as desrtroying the continuation object?

 
An issue I stumbled upon during implementation was not being able to do callcc() on a method. I think an overload of callcc specifically for method pointers (in parallel with the std::function constructor set) will be needed. This is as the std::continuation&& parameter gets prepended to the args of callcc when calling the provided function. If the provided function is a std::function wrapping a method pointer its operator() will expect to get the 'this' of the object to call and instead it will get the std::continuation prepended by callcc!

// Example:

class MyClass {
    void fun(std::continuation&& cont);
};

MyClass myObj;

callcc(&MyClass::fun, myObj);    // This is what you want to write, but callcc does not take method pointers.

// So you may try this:

callcc(std::function<void(MyClass&, std::continuation&&)>(&MyClass::fun), myObj);   // Mismatched types as callcc gets the order wrong.

// Instead you have to conjure up a lambda:

callcc([&](std::continuation&& cont) { myObj.fun(std::move(cont); });


While the lambda solution works I think it is quite weak not to allow the first version.

I'm too busy - so it didn't got the highest prio/keep in mind I do it at my freetime
I do too, so we're in the same sitation there...

 In your next reply you again point at the _function_ based version of callcc, which is what caused my concern in the first place. Maybe you just pasted the wrong snippet? If not, here it is again: The problem is that as callcc prepends the continuation to the parameter list of the provided Fn there is a problem for a std::function encapsulating a method (which mandates a compatible object as its first invoktion parameter).
 

Oliver Kowalke

unread,
Mar 4, 2017, 11:14:18 AM3/4/17
to std-pr...@isocpp.org
2017-03-04 15:27 GMT+01:00 Bengt Gustafsson <bengt.gu...@beamways.com>:

But I don't understand the need of data transfer at this low abstraction level. After all, these are not threads, so there is no asynchronism to think about.

P0534 is symmetric context switching, e.g. no coupling between caller and callee as it is for generators/coroutines.
Exactly, that's why I asked why data transfer is needed _on the call/cc level_. I showed in my example that you can do data transfer on top of this (at the generator level) with essentially no overhead. Giovanni pointed out that there may be the overhead of one register save/restore, and if this the performance gain you are after I think the price of a scary "void*" style interface (hidden behind an unsafe cast) is not worth it, and if the access is to be tested the performance cost is definitely more than a register save.

I don't see your point - accessing a register if verry fast (1 CPU cycle I guess).
The data transfer as proposed in P0534 (and used for coroutine and fiber implementation) does provide some kind of type-safety and no overhead if no data are transferred.
 
 And you don't know the identity of the continuation returned from operator() in your case, what is the difference? The advantage of the in situ version is however obvious: You can't forget doing the assignment, and thereby leave the continuation object stale. 

Suppose you have contoinuations c1, c2:

c1 = callcc( foo);
c2 = callcc( bar);

- in main() you resume c1: c1();
- c1 executes foo
- in foo you resume c2: c2()
- c2 executes bar
- in bar you jump to main()
- that means you return from c1() in main()
- if you call c1() in main() you do resume bar() instead of foo()

 

 
Also it would be interesting to know when this rather strange functionality is useful.

described in P0534:
you can invoke a new function after resuming the context that throws an exception
 
It is a bit hard to understsand this sentense.

context switching is a bit mind twisiting
 
Do you mean to resume a context after it has thrown an exception?

no - resume the context and then, if you are in the resumed context, create a new stack frame and execute your supplied function (that might throw)
 
 
for instance you want abort and unwind the stack - instead to check for abort each time the context is resumed you call invoke-ontop functionality to execute the throwing abort function only at the time point you know you want to abort the continuation/context
I think you may be saying that once in a while one party injects code that throws if an abort flag is set into another piece of code on the other side of the continuation.

yes
 
Or maybe you test the abort flag first and then injects a function which just throws an exception. Well that would be a way to get rid of the continuation and properly unwind its stack, but didn't you have a specific way of doing that, such as desrtroying the continuation object?

throwing an exeception was only one use case - infact if a suspended continuation has to be destroyed (instance goes out of scope) out of a deep callstack - the current implementation executes a function ontop of those continuation that throws a speciall exception that unwinds the stakc of the continuation and jumps back to the caller.
 

Bengt Gustafsson

unread,
Mar 4, 2017, 2:04:16 PM3/4/17
to ISO C++ Standard - Future Proposals


Den lördag 4 mars 2017 kl. 17:14:18 UTC+1 skrev Oliver Kowalke:
2017-03-04 15:27 GMT+01:00 Bengt Gustafsson <bengt.gu...@beamways.com>:

But I don't understand the need of data transfer at this low abstraction level. After all, these are not threads, so there is no asynchronism to think about.

P0534 is symmetric context switching, e.g. no coupling between caller and callee as it is for generators/coroutines.
Exactly, that's why I asked why data transfer is needed _on the call/cc level_. I showed in my example that you can do data transfer on top of this (at the generator level) with essentially no overhead. Giovanni pointed out that there may be the overhead of one register save/restore, and if this the performance gain you are after I think the price of a scary "void*" style interface (hidden behind an unsafe cast) is not worth it, and if the access is to be tested the performance cost is definitely more than a register save.

I don't see your point - accessing a register if verry fast (1 CPU cycle I guess).
The point is that if the feature does not have a large performance gain compared to not having it, and if it does not avoid a complex synchronization problem then why have it? I don't see that you have shown any major performance or synchronization gain.

 
The data transfer as proposed in P0534 (and used for coroutine and fiber implementation) does provide some kind of type-safety and no overhead if no data are transferred.
While ideas in the direction of my Generator class offers compile time type safety and negligable overhead even if used! Not to mention a smaller API.
 
 
 And you don't know the identity of the continuation returned from operator() in your case, what is the difference? The advantage of the in situ version is however obvious: You can't forget doing the assignment, and thereby leave the continuation object stale. 

Suppose you have contoinuations c1, c2:

c1 = callcc( foo);
c2 = callcc( bar);

- in main() you resume c1: c1();
This must mean that both foo and bar have transfered control back to main which in some later code calls c1().
- c1 executes foo
continuing foo after its first context switch. 
 
 
- in foo you resume c2: c2()
ok, a bit odd but doable. 

- c2 executes bar
continuing bar after its first context switch.
 
- in bar you jump to main()
Unless you involve setjmp/longjmp I don't see a way fot this to happen... and those guys are not C++ safe anyway. What can happen is that foo gives bar access to its stored continuation and bar uses this to transfer control to main after its c1() call.
And sure enough c1 now refers to bar after its second context switch. Yes this seems to be what you refer to.
 
- that means you return from c1() in main()
- if you call c1() in main() you do resume bar() instead of foo()
Yes. This can happen if you store the continuations in global variables where other functions can reach them. Isn't this why we want to abstract the continuation level into higher level abstractions which are easier to understand and where these types of
oddities are easy to avoid.

The only gain with the assignment is as a reminder of "next time this continuation will continue executing somewhere else". But as that always happens and you can not make something else happen by NOT doing the assignment (except dropping a valuable asset) it seems rather error prone...


 

 
Also it would be interesting to know when this rather strange functionality is useful.

described in P0534:
you can invoke a new function after resuming the context that throws an exception
 
It is a bit hard to understsand this sentense.

context switching is a bit mind twisiting
 
Do you mean to resume a context after it has thrown an exception?

no - resume the context and then, if you are in the resumed context, create a new stack frame and execute your supplied function (that might throw)
OK, I understand what it does, my question was why this is useful. 
 
 
for instance you want abort and unwind the stack - instead to check for abort each time the context is resumed you call invoke-ontop functionality to execute the throwing abort function only at the time point you know you want to abort the continuation/context
I think you may be saying that once in a while one party injects code that throws if an abort flag is set into another piece of code on the other side of the continuation.

yes
 
Or maybe you test the abort flag first and then injects a function which just throws an exception. Well that would be a way to get rid of the continuation and properly unwind its stack, but didn't you have a specific way of doing that, such as desrtroying the continuation object?

throwing an exeception was only one use case - infact if a suspended continuation has to be destroyed (instance goes out of scope) out of a deep callstack - the current implementation executes a function ontop of those continuation that throws a speciall exception that unwinds the stakc of the continuation and jumps back to the caller.
Yes, that is how I understood it. This is what would happen if you forget to assign the returned continuation to your variable, I assume ;-)

So the only use case for invoke_on_top() that you have produced is in fact equivalent to destroying the continuation object. Given that you can't access data in underlying stack frames I fail to see a difference between these two:

continuation c;

fun()
c()

and
c.invoke_on_top(fun);

Well, there may be some difference if fun throws, but isn't it unlikely that the code dormant in c would have suitable catch clauses for throws in an unrelated function fun.

Ok, maybe you only see this as a method of throwing an exception in the other stack, if so maybe it would be better to have an explicit mechanism for this:

template<typename EX, typename.... Args> continuation::throw_in(Args...);

This just swaps the stacks and then executes `throw EX(args...)`

The advantage of this approach is that it is obvious what it does and it does not invite to inject arbitrary function calls (which may throw exceptions without catching them) onto some other stack not prepared for this.,

I would have thought that the main use would be to allow a pool of fibers to be used for executing arbitrary code. The main reason for not doing new callcc() calls each time would be to avoid having to allocate stacks each time. If this is the use case it seems that the stack rather than the continuation is the object to preserve and pool. It may be that this is the functionality Nicol was asking for.
 
 

Oliver Kowalke

unread,
Mar 4, 2017, 3:56:53 PM3/4/17
to std-pr...@isocpp.org
2017-03-04 20:04 GMT+01:00 Bengt Gustafsson <bengt.gu...@beamways.com>:
I don't see your point - accessing a register if verry fast (1 CPU cycle I guess).
The point is that if the feature does not have a large performance gain compared to not having it, and if it does not avoid a complex synchronization problem then why have it? I don't see that you have shown any major performance or synchronization gain.

I don't understand what you try to tell me
 

The data transfer as proposed in P0534 (and used for coroutine and fiber implementation) does provide some kind of type-safety and no overhead if no data are transferred.
While ideas in the direction of my Generator class offers compile time type safety and negligable overhead even if used!

 generators provide asynchronous context switching which is always less efficient than synchronous
 
Not to mention a smaller API.

it's not
 
- in foo you resume c2: c2()
ok, a bit odd but doable. 

why is it odd?
 

- in bar you jump to main()
Unless you involve setjmp/longjmp I don't see a way fot this to happen

callcc() does similiar things like setjmp/longjmp - actually callcc() creates a new stack and a new context and exchanges stack and instruction pointer
 
What can happen is that foo gives bar access to its stored continuation and bar uses this to transfer control to main after its c1() call.

I don't get it

foo can not give its continuation to bar while foo is running
foo is supended by calling callcc() or continuation::operator() and the invocation of this functions generate a continuation of foo
 
And sure enough c1 now refers to bar after its second context switch. Yes this seems to be what you refer to.
 
- that means you return from c1() in main()
- if you call c1() in main() you do resume bar() instead of foo()
Yes. This can happen if you store the continuations in global variables

no, global vars are not needed
 
Isn't this why we want to abstract the continuation level into higher level abstractions which are easier to understand and where these types of
oddities are easy to avoid.

I would say no - higher level astractions defined how continuations interact

 
The only gain with the assignment is as a reminder of "next time this continuation will continue executing somewhere else". But as that always happens and you can not make something else happen by NOT doing the assignment (except dropping a valuable asset) it seems rather error prone...

I don't get it
 
no - resume the context and then, if you are in the resumed context, create a new stack frame and execute your supplied function (that might throw)
OK, I understand what it does, my question was why this is useful. 

yes
 
Yes, that is how I understood it. This is what would happen if you forget to assign the returned continuation to your variable, I assume ;-)

right
 
So the only use case for invoke_on_top() that you have produced is in fact equivalent to destroying the continuation object.

no
 
Given that you can't access data in underlying stack frames

you can pass data from the fucntion executed ontop of the context to the underlying stack frame
 
continuation c;

fun()
c()

fun() is executed on the current stack (and in the current context - lets say main() for instance)
 
c() resumes c, e.g switches execution to the stack and context c is related to

and
c.invoke_on_top(fun);

resumes c and invokes fun on the stack c is related to == fun() is executed in the context of c

 
Well, there may be some difference if fun throws,

if fun() throws

fun()
c()
-> c() is not executed; exeception is thrown in the context of main() for instance

c.invoke_on_top(fun);
-> exeception is thrown inside c and will terminate c if not catched

Ok, maybe you only see this as a method of throwing an exception in the other stack,

no

I would have thought that the main use would be to allow a pool of fibers to be used for executing arbitrary code.

you nixing up concepts - callcc() is not a fiber, but fibers can be implemented using callcc() (see boost.fiber as an example)
 
The main reason for not doing new callcc() calls each time would be to avoid having to allocate stacks each time.

callcc() is factory function of continuations, it generates a new stack and context
 
If this is the use case it seems that the stack rather than the continuation is the object to preserve and pool.

the stack is actually the continuation - if continuation::operator() is called the stack pointer (stored inside continuation) is assigned to the stack pointer and the instruction pointe ris poped from it
 

Giovanni Piero Deretta

unread,
Mar 4, 2017, 5:16:26 PM3/4/17
to std-pr...@isocpp.org
On Sat, Mar 4, 2017 at 7:04 PM, Bengt Gustafsson
<bengt.gu...@beamways.com> wrote:
>
> Den lördag 4 mars 2017 kl. 17:14:18 UTC+1 skrev Oliver Kowalke:
>>
>> 2017-03-04 15:27 GMT+01:00 Bengt Gustafsson <bengt.gu...@beamways.com>:
>>>>
>>>>
>>>>> But I don't understand the need of data transfer at this low
>>>>> abstraction level. After all, these are not threads, so there is no
>>>>> asynchronism to think about.
>>>>
>>>>
>>>> P0534 is symmetric context switching, e.g. no coupling between caller
>>>> and callee as it is for generators/coroutines.
>>>
>>> Exactly, that's why I asked why data transfer is needed _on the call/cc
>>> level_. I showed in my example that you can do data transfer on top of this
>>> (at the generator level) with essentially no overhead. Giovanni pointed out
>>> that there may be the overhead of one register save/restore, and if this the
>>> performance gain you are after I think the price of a scary "void*" style
>>> interface (hidden behind an unsafe cast) is not worth it, and if the access
>>> is to be tested the performance cost is definitely more than a register
>>> save.
>>
>>
>> I don't see your point - accessing a register if verry fast (1 CPU cycle I
>> guess).
>
> The point is that if the feature does not have a large performance gain
> compared to not having it, and if it does not avoid a complex
> synchronization problem then why have it? I don't see that you have shown
> any major performance or synchronization gain.
>

I haven't seen Oliver's implementation, but the feature can be
implemented with negative cost (as I mentioned elsethread): you simply
skip saving and restoring a register and use it to pass a pointer
though.

Implementing the same feature non intrusively is more expensive as you
need to go through memory and require coordination between the threads
of execution (i.e. sharing some memory cell), which might be non
trivial when the

There is a strong parallel between a function call and a continuation
invocation [1]. They both are a controlled jump to another memory
location, possibly while passing parameters though, and a program will
benefit by both being as fast as possible when inlining is not
desirable or possible.

[1] given a simple program transformation they are in fact exactly the
same thing.

[...]
>>
>> throwing an exeception was only one use case - infact if a suspended
>> continuation has to be destroyed (instance goes out of scope) out of a deep
>> callstack - the current implementation executes a function ontop of those
>> continuation that throws a speciall exception that unwinds the stakc of the
>> continuation and jumps back to the caller.
>
> Yes, that is how I understood it. This is what would happen if you forget to
> assign the returned continuation to your variable, I assume ;-)
>
> So the only use case for invoke_on_top() that you have produced is in fact
> equivalent to destroying the continuation object.

The killer application is of course is implement waiting of events.
For example, waiting on an std::future:

template<class T>
T await(std::future<T> f, continuation& yield) {
yield = yield(invoke_on_top, [&](auto&& c) { f =
f.then([c=move(c)] (auto f){ c(); return f.get(); }); return
continuation{} });
return f.get();
}

With invoke on top, waiting is trivial; without it is a pain to
implement; witness the epicycles that the other coroutine proposal
needs to make this work. Await doesn't require neither explicit
support from continuation nor std::future to work [2].

Other uses:

* implementing a (transparent) task scheduling system on top of
continuations: pop 'next' from ready queue, invoke-on-top of 'next'
and push captured 'previous' on back of queue. Return into 'next'.


* with typed continuations, it allows capturing the thread of control
with a different continuation type.

* continuation splicing:
switch from thread of control 'a' to 'b', but return to 'b' a
separate thread of control 'c' instead of 'a'. This can be used to
implement complex control flow.

In general, invoke-on-top is useful in any case in which the current
flow of control need to capture its own continuation without spawning
a new flow of control. It is the well behaved equivalent of
getcontext+setcontext (as opposed to swapcontext), except that the
code in between is executed on a separate stack (which has implication
for exceptions and concurrency).

invoke_on_top can of course be implemented on top of an implementation
without it, especially if a parameter passing feature is available,
but it requires cooperation of the target continuation and it is less
efficient. Considering that an implementation will need something like
it anyway to implement setup, exception forwarding and termination, it
makes sense to expose it to the user.

[2] Having a non-allocating, dismissable, variant of 'then' would be
better, but that's another story.
-- gpd

Bengt Gustafsson

unread,
Mar 4, 2017, 6:13:11 PM3/4/17
to ISO C++ Standard - Future Proposals
Note: Giovanni's reply popped up while I was typing. There may be some more good argumentation there...


Den lördag 4 mars 2017 kl. 21:56:53 UTC+1 skrev Oliver Kowalke:
2017-03-04 20:04 GMT+01:00 Bengt Gustafsson <bengt.gu...@beamways.com>:
I don't see your point - accessing a register if verry fast (1 CPU cycle I guess).
The point is that if the feature does not have a large performance gain compared to not having it, and if it does not avoid a complex synchronization problem then why have it? I don't see that you have shown any major performance or synchronization gain.

I don't understand what you try to tell me
 
That mixing in the data passing into the continuation class is not a good idea. I have asked you to come up with an explanation why it would be a good idea, "because we can" is not good enough as this data can easily be passed without any support from the continuation class.


 
 

The data transfer as proposed in P0534 (and used for coroutine and fiber implementation) does provide some kind of type-safety and no overhead if no data are transferred.
While ideas in the direction of my Generator class offers compile time type safety and negligable overhead even if used!

 generators provide asynchronous context switching which is always less efficient than synchronous

I am talking about the generators according to my continuation based class.
 
 
Not to mention a smaller API.

it's not
How can not having a get method not be a smaller API?

 
 
- in foo you resume c2: c2()
ok, a bit odd but doable. 

why is it odd?
 
I just ment that it doesn't follow any of the usual patterns that continuations are used for: generators, data flow machines, fiber pools.
 
 

- in bar you jump to main()
Unless you involve setjmp/longjmp I don't see a way fot this to happen

callcc() does similiar things like setjmp/longjmp - actually callcc() creates a new stack and a new context and exchanges stack and instruction pointer
 
What can happen is that foo gives bar access to its stored continuation and bar uses this to transfer control to main after its c1() call.

I don't get it
I could have messed up but I tried to follow the order of events you described and came to the conclusion that foo got the context returned to it from its first continuation invokation, so for bar to invoke it it must have access to the variable where foo stored it.
 

foo can not give its continuation to bar while foo is running
"Its continuation" is the one it got returned from its invokation, of course it can store it somewhere for bar to get.
 
foo is supended by calling callcc() or continuation::operator() and the invocation of this functions generate a continuation of foo
 
And sure enough c1 now refers to bar after its second context switch. Yes this seems to be what you refer to.
 
- that means you return from c1() in main()
- if you call c1() in main() you do resume bar() instead of foo()
Yes. This can happen if you store the continuations in global variables

no, global vars are not needed
ok, could be members then.
 
 
Isn't this why we want to abstract the continuation level into higher level abstractions which are easier to understand and where these types of
oddities are easy to avoid.

I would say no - higher level astractions defined how continuations interact
well maybe, but the application code using those abstractions don't see the continuations, well whatever.
 

 
The only gain with the assignment is as a reminder of "next time this continuation will continue executing somewhere else". But as that always happens and you can not make something else happen by NOT doing the assignment (except dropping a valuable asset) it seems rather error prone...

I don't get it
If you do myContinuation() instead of myContinuation = myContinuation() the other stack is unwound when the temporary object is destroyed, no error message at compile time or runtime, that's what I call error prone.
 
 
no - resume the context and then, if you are in the resumed context, create a new stack frame and execute your supplied function (that might throw)
OK, I understand what it does, my question was why this is useful. 

yes
yes? 
 
Yes, that is how I understood it. This is what would happen if you forget to assign the returned continuation to your variable, I assume ;-)

right
 
So the only use case for invoke_on_top() that you have produced is in fact equivalent to destroying the continuation object.

no
what other use case have you produced, I can't find any...
 
 
Given that you can't access data in underlying stack frames

you can pass data from the fucntion executed ontop of the context to the underlying stack frame

How?

The last piece of code that ran in the underlying stack frame was the continuation invokation. This returns the other continuation, as far as that code knows. Now you insert a function "on top", which I assume must return a continuation to fulfil the obligation of the unsuspecting code that invoked the continuation. The only way to not have to return a continuation there would be to throw. Well of course this would be a possible way to pass data to the invoker, like this:

try {
    myContinuation();    // This will throw
}catch (SomeData& data);
// Use data here.

and then the other side injects a function throwing a SomeData instance. Apart from the questions about life time of the thrown object this seems incredibly clumsy, which is a reason why I asked for concrete use case apart from throwing.


 
 
continuation c;

fun()
c()

fun() is executed on the current stack (and in the current context - lets say main() for instance)
 
c() resumes c, e.g switches execution to the stack and context c is related to

and
c.invoke_on_top(fun);

resumes c and invokes fun on the stack c is related to == fun() is executed in the context of c
Yes, but all visible effects except the addresses of local variables in fun() would be the same. 

 
Well, there may be some difference if fun throws,

if fun() throws

fun()
c()
-> c() is not executed; exeception is thrown in the context of main() for instance

c.invoke_on_top(fun);
-> exeception is thrown inside c and will terminate c if not catched

Ok, maybe you only see this as a method of throwing an exception in the other stack,

no
Still the example only revolves around throwing and where the unwinding of that exception continues.
 

I would have thought that the main use would be to allow a pool of fibers to be used for executing arbitrary code.

you nixing up concepts - callcc() is not a fiber, but fibers can be implemented using callcc() (see boost.fiber as an example)

No I'm not, I was refering to callcc() calling a function containing only a loop with a yeild, and then executing fibers on that stack using invoke_on_top.
 
 
The main reason for not doing new callcc() calls each time would be to avoid having to allocate stacks each time.

callcc() is factory function of continuations, it generates a new stack and context

exactly, so to avoid having to allocate new stacks you would have to avoid repeated calls to callcc(), but with invoke_on_top you can inject any code for execution in that stack. However, the same effect can be achieved using a std::function object that said loop executes between each continuation call:

std::function<void()> work;
void fiber_caller(continuation cont)
{
    for(;;) {
       cont();
       work();
    }
}

To use it just set work to a function and invoke the continuation returned from callcc. Same stack, much work.
        

 
If this is the use case it seems that the stack rather than the continuation is the object to preserve and pool.

the stack is actually the continuation - if continuation::operator() is called the stack pointer (stored inside continuation) is assigned to the stack pointer and the instruction pointe ris poped from it
Sure, viewing the continuation as the stack I think is a good way to gain understanding. Then moving it around makes even less sense to me.

Giovanni's reply popped up while I was typing. There may be some more good argumentation there...
 
 

Bryce Glover

unread,
Mar 4, 2017, 8:38:30 PM3/4/17
to Bengt Gustafsson, std-pr...@isocpp.org
On Mar 3, 2017, at 7:36 AM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:

I don't know what you mean by trampoline in this context. I checked wikipedia link but I didn't get it...


     Yeah, sorry, that article’s not as descriptive as one would hope, is it?  Maybe this StackOverflow question will help you understand what I was trying to convey, or perhaps I was just confusing the idea of a trampoline with the similar concepts of thunks and/or shims.  The example I usually tend to think of is the Objective-C run-time library’s `objc_msgSend()` function (which I’ve found is referred to as a trampoline) due to my rudimentary familiarity with Apple platforms, but I’m not sure how helpful that might be to you…


This aside, do you see a reason for this being a tag type and operator() rather than a method call?


Honestly, I don’t have enough of an opinion on or enough stake in this argument to care all that much either way.  I just see continuations as an interesting construct that I might use some time in the future.  


I thought about this more and it seems that it may have to do with implementing a fiber pool. The invoked function is the body of the next fiber to execute on the stack of the continuation and the target of the ccllcc is just a function like this:

void fiber_base(continuation&& cont, bool& stop)
{
   continuation c = std::move(cont);
   while (!stop)
       c = c();
}

then the party starting the fiber_base with callcc can invoke anything on this stack and when this anything returns the loop goes back to wait for the next task.

This idea is just speculation on my part, but it seems to show a use case (unless the stack itself is made a first class object which can be reused for multiple callcc calls which I would find more logical). 


That’s another way of looking at it, I suppose…

— Bryce Glover


Den 2017-03-02 kl. 23:21, skrev Bryce Glover:
(snipped…)
-- 
Bengt Gustafsson
CEO, Beamways AB
Westmansgatan 37
582 16 Linköping, Sweden
+46 (705) 338259
Skype: benke_g
www.beamways.com
Reply all
Reply to author
Forward
0 new messages