This paper is much closer to finished than the previous half arsed paper posted here at Vicente's request. It is also pretty much completely rewritten, and Vicente no longer wishes to author it.The highlight of this paper is proposing native C++ macros as a better solution for implementing try, co_await, co_yield and co_return. These become:
- std::try#(T)
- std::coroutines::await#(T)
- std::coroutines::yield#(T)
- std::coroutines::return#(T)
Which I think everybody can agree is much nicer. Just plonk a "using namespace std::coroutines;" at the top of your coroutine and it's find and replace replacement, plus you can axe quite a few pages from the Coroutines TS.
Anyway I expect this discussion thread will be lively. From the previous discussion thread, people either love or hate boilerplate injection into calling scopes. But this is, at least, a generic library based alternative to constantly having to add new operators and keywords to the C++ language going into the future. Hate it as much as you like, EWG has better things to do than standardise boilerplate into the compiler.
This paper is much closer to finished than the previous half arsed paper posted here at Vicente's request. It is also pretty much completely rewritten, and Vicente no longer wishes to author it.The highlight of this paper is proposing native C++ macros as a better solution for implementing try, co_await, co_yield and co_return. These become:
- std::try#(T)
- std::coroutines::await#(T)
- std::coroutines::yield#(T)
- std::coroutines::return#(T)
Which I think everybody can agree is much nicer. Just plonk a "using namespace std::coroutines;" at the top of your coroutine and it's find and replace replacement, plus you can axe quite a few pages from the Coroutines TS.
Anyway I expect this discussion thread will be lively. From the previous discussion thread, people either love or hate boilerplate injection into calling scopes. But this is, at least, a generic library based alternative to constantly having to add new operators and keywords to the C++ language going into the future. Hate it as much as you like, EWG has better things to do than standardise boilerplate into the compiler.
Hi, Niall. Seeing as you dropped my name in your paper, I feel compelled to respond :)On Thursday, 12 October 2017 14:02:24 UTC+13, Niall Douglas wrote:This paper is much closer to finished than the previous half arsed paper posted here at Vicente's request. It is also pretty much completely rewritten, and Vicente no longer wishes to author it.The highlight of this paper is proposing native C++ macros as a better solution for implementing try, co_await, co_yield and co_return. These become:
- std::try#(T)
- std::coroutines::await#(T)
- std::coroutines::yield#(T)
- std::coroutines::return#(T)
Which I think everybody can agree is much nicer. Just plonk a "using namespace std::coroutines;" at the top of your coroutine and it's find and replace replacement, plus you can axe quite a few pages from the Coroutines TS.Heh. As Nicol rather promptly pointed out, it's not that simple.You throw around a lot of "everybody can agree" and "obvious reasons", but those are the very things that need to be justified in your paper.For example, in what way is using co_await on an optional "not a true coroutine await"? Why, exactly, does it need to be "nipped in the bud"? I'm not necessarily arguing in favour of using co_await for that purpose, but I really don't see what's so abhorrent about it.
Regards,Toby.
On Wednesday, October 11, 2017 at 10:08:32 PM UTC-4, Toby Allsopp wrote:Hi, Niall. Seeing as you dropped my name in your paper, I feel compelled to respond :)On Thursday, 12 October 2017 14:02:24 UTC+13, Niall Douglas wrote:This paper is much closer to finished than the previous half arsed paper posted here at Vicente's request. It is also pretty much completely rewritten, and Vicente no longer wishes to author it.The highlight of this paper is proposing native C++ macros as a better solution for implementing try, co_await, co_yield and co_return. These become:
- std::try#(T)
- std::coroutines::await#(T)
- std::coroutines::yield#(T)
- std::coroutines::return#(T)
Which I think everybody can agree is much nicer. Just plonk a "using namespace std::coroutines;" at the top of your coroutine and it's find and replace replacement, plus you can axe quite a few pages from the Coroutines TS.Heh. As Nicol rather promptly pointed out, it's not that simple.You throw around a lot of "everybody can agree" and "obvious reasons", but those are the very things that need to be justified in your paper.For example, in what way is using co_await on an optional "not a true coroutine await"? Why, exactly, does it need to be "nipped in the bud"? I'm not necessarily arguing in favour of using co_await for that purpose, but I really don't see what's so abhorrent about it.I can explain that:1. If you `co_await` in a function, any return must be a `co_return` as well.
2. As I pointed out in my post, using `co_await` adds a bunch of machinery to your code. Among the practical effects of that machinery is that guaranteed elision no longer works. The promise object in the coroutine manages the return value object, and it cannot share its storage with the caller's return value. So if you `co_return` a prvalue, that will be used to initialize the return value object in the promise. And when the eventual caller gets the return value, it will have to copy/move it from the promise.
3. If you're using `co_await` for optionals or expected, you cannot simultaneously use `co_await` for waiting on other futures too. That is, if your function returns `future<optional<T>>`, you can't use `co_await` on something that returns an `optional<T>`. The two "channels" of communication can't really coexist within the same function. Having a `co_try` would allow you to make `future<optional<T>>` work.
The Coroutines TS is a lot more complicated than you make it out to be. The presence of any of the `co_*` keywords in a function doesn't merely change the nature of the expression; it transforms the whole nature of the function. It turns a regular function into a coroutine function, which implicitly wraps the entire thing within a scope, adds a promise object, and does a host of other things.
Merely "injecting boilerplate" into an expression doesn't do that.Furthermore, a coroutine function cannot issue a normal return statement, since its return value object is managed by the coroutine's promise object. Your mere "boilerplate injection" idea can't make you get a compile error if you return from a function.
And speaking of which, this is a good example of where your `try` idea (in either form) fails: it doesn't take into account the possibility of a coroutine function performing the `try` operation. In form 1, you're effectively requiring the compiler to do arbitrary look-ahead to see if there's a `co_*` keyword in use, and if it is, then it has to use `co_return`. In form 2, `try#` has no way to know whether it's a coroutine or not.
Heh. As Nicol rather promptly pointed out, it's not that simple.
You throw around a lot of "everybody can agree" and "obvious reasons", but those are the very things that need to be justified in your paper.
For example, in what way is using co_await on an optional "not a true coroutine await"? Why, exactly, does it need to be "nipped in the bud"? I'm not necessarily arguing in favour of using co_await for that purpose, but I really don't see what's so abhorrent about it.
Anyway I expect this discussion thread will be lively. From the previous discussion thread, people either love or hate boilerplate injection into calling scopes. But this is, at least, a generic library based alternative to constantly having to add new operators and keywords to the C++ language going into the future. Hate it as much as you like, EWG has better things to do than standardise boilerplate into the compiler.I like the idea of being able to implement interesting compile-time transformations, but I see your proposed native macros as not powerful enough. I think something that builds on static reflection of a function body at the statement and expression level, similar to the metaclasses stuff, will give better results and allow for more interesting transformations.
For example, I can't see how to get something like Haskell's do notation using your native macros. For that you need a way to capture the continuation of the calling function as a callable object, which means effectively wrapping it in a lambda.
1. If you `co_await` in a function, any return must be a `co_return` as well.That's true, but I don't get your point. Is the problem the inconvenience of having to change the function body in potentially many places if you decide to use co_await?I don't believe this requirement is fundamental to the Coroutines TS.
2. As I pointed out in my post, using `co_await` adds a bunch of machinery to your code. Among the practical effects of that machinery is that guaranteed elision no longer works. The promise object in the coroutine manages the return value object, and it cannot share its storage with the caller's return value. So if you `co_return` a prvalue, that will be used to initialize the return value object in the promise. And when the eventual caller gets the return value, it will have to copy/move it from the promise.This is a very good point, thanks for pointing it out.Niall - put this in your paper as justification for nipping co_await optional in the bud!
3. If you're using `co_await` for optionals or expected, you cannot simultaneously use `co_await` for waiting on other futures too. That is, if your function returns `future<optional<T>>`, you can't use `co_await` on something that returns an `optional<T>`. The two "channels" of communication can't really coexist within the same function. Having a `co_try` would allow you to make `future<optional<T>>` work.Hmm, this is an interesting point. It reminds me of monad transformers.I suspect co_await on an optional in a coroutine returning future<optional<T>> could be made to work, but the composition would be awkward, i.e. the future's promise would need to know about optional or optional-like things.
On 12 October 2017 at 15:55, Niall Douglas <nialldo...@gmail.com> wrote:A couple of remarks:
> Herb's metaclasses could be a solution, but that's a very big proposal very
> far out, and one which still wouldn't eliminate the need for C macros. So I
> float the idea of native C++ macros, see if it sticks. It's one solution to
> the problem. Another is letting you define your own keywords local to a
> namespace, but I know Bjarne hates that idea.
1) I have toyed with the idea of "inject into the parent scope" many
times and hinted at it on this forum multiple
times. The way I expressed it was with an "inline template", that is
inline template <class T>
void f(T t) // yes, the return type is stupid, this is not a function,
it doesn't return
{
if (stars_are_properly_aligned(t))
return foo(t); // this returns from the surrounding scope
}
and using it would look like a function call,
int g()
{
f(42);
}
2) this is indeed one remaining use of macros for which we have no
superior replacement. The inline-template idea I've toyed with
doesn't suggest at the call site that injection will happen,
whereas
your hash-macro perhaps would/should.
3) I think it does harm to the idea if it suggests tokens to be
injected into the surrounding scope. It seems much more palatable
to me if the "native-macro" establishes a new block and just injects
that block into the surrounding scope. In a similar vein,
the code in the native-macro should be valid, and if dependent, should
require that there's at least one specialication for which it's
valid. These were the things that were required for if-constexpr to be
acceptable, I expect the same requirements to arise here.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAFk2RUYXya9jkfdW51HPPUaGH79K9%3DimHZtb-MS5sVU%2BdSn9wQ%40mail.gmail.com.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
A couple of remarks:
1) I have toyed with the idea of "inject into the parent scope" many
times and hinted at it on this forum multiple
times. The way I expressed it was with an "inline template", that is
inline template <class T>
void f(T t) // yes, the return type is stupid, this is not a function,
it doesn't return
{
if (stars_are_properly_aligned(t))
return foo(t); // this returns from the surrounding scope
}
and using it would look like a function call,
int g()
{
f(42);
}
2) this is indeed one remaining use of macros for which we have no
superior replacement. The inline-template idea I've toyed with
doesn't suggest at the call site that injection will happen, whereas
your hash-macro perhaps would/should.
3) I think it does harm to the idea if it suggests tokens to be
injected into the surrounding scope. It seems much more palatable
to me if the "native-macro" establishes a new block and just injects
that block into the surrounding scope. In a similar vein,
the code in the native-macro should be valid, and if dependent, should
require that there's at least one specialication for which it's
valid. These were the things that were required for if-constexpr to be
acceptable, I expect the same requirements to arise here.
> Would this not prevent injecting new variable declarations?
Yes, it would. Some might consider that a feature rather than a bug.
The downside is of course
that you can't inject a nifty RAII-variable into the surrounding
scope. I don't know how big a deal
the scope would be here; it was certainly of utmost importance for
if-constexpr. The best way to
find out is to propose this and see what EWG says. But at any rate,
injection of tokens seems
like something that will meet fire and brimstone.
> Would this not prevent injecting new variable declarations?
Yes, it would. Some might consider that a feature rather than a bug.One of the big advantages of C macros is that they can inject partial scopes e.g. BEGIN_BITFIELD(foo) ... END_BITFIELD(foo), and RAII variables for firing code execution on exit.Removing that facility would keep C macros being used in new code.
The downside is of course
that you can't inject a nifty RAII-variable into the surrounding
scope. I don't know how big a deal
the scope would be here; it was certainly of utmost importance for
if-constexpr. The best way to
find out is to propose this and see what EWG says. But at any rate,
injection of tokens seems
like something that will meet fire and brimstone.I don't love it either for the record. But it is well understood by all, there is little danger of unanticipated consequences in adopting it (well, ones worse than C macros).And a truly better solution is actually quite hard to achieve in any reasonable timespan.One thing I'd like is if these macros had constexpr-only function pointer behaviours i.e. within constexpr, you can return them and pass them around between constexpr functions, with the token injection only occurring upon invocation. I could see that as being really very useful. But it would be much harder for compiler vendors to implement as they're need to recursively reconstruct the AST being executed by constexpr, so I left that idea out.Perhaps the partial parsing idea like a template you had might help with that, once parsed it's useful to constexpr, though one loses the ability to inject partial scopes which I think is important for total C macro replacement.Do you know what is really weird with these C++ macros, it just occurred to me? These C++ macros are actually how MSVC has historically implemented templates: token injection. I am effectively proposing that here.
Niall
Heh. As Nicol rather promptly pointed out, it's not that simple.Coroutines are actually very easy to implement under the bonnet. Getting them to compile into efficient code is much harder, but some implementation or other probably can be hacked into a compiler within a week. I say this as a former clang/LLVM port maintainer for a platform, and I don't claim the hacked implementation would be pretty nor wise.
For example, in what way is using co_await on an optional "not a true coroutine await"? Why, exactly, does it need to be "nipped in the bud"? I'm not necessarily arguing in favour of using co_await for that purpose, but I really don't see what's so abhorrent about it.
I'm very keen that we avoid hacks instead of doing things properly. In many ways the co_* keywords are themselves a hack, some way of marking a function as being a special "coroutine function" was needed plus boilerplate needed to be injected.
For example, I can't see how to get something like Haskell's do notation using your native macros. For that you need a way to capture the continuation of the calling function as a callable object, which means effectively wrapping it in a lambda.Vicente has a paper on implementing Haskell do in the works. It is sufficiently complex it's unachievable for C++ 20. In the meantime, these native C++ macros are (a) extremely easy to implement by the compiler vendor and (b) would be relatively uncontroversial to design seeing how closely they replicate C macros (but without the problems). They're achievable for C++ 20, in time for the Coroutines TS and Expected.
Vicente has a paper on implementing Haskell do in the works. It is sufficiently complex it's unachievable for C++ 20. In
1. If you `co_await` in a function, any return must be a `co_return` as well.That's true, but I don't get your point. Is the problem the inconvenience of having to change the function body in potentially many places if you decide to use co_await?I don't believe this requirement is fundamental to the Coroutines TS.It's more that a coroutinised function is not a normal function. It can't behave the same way, it can't be compiled the same way, and it definitely can't be invoked the same way. This all greatly limits the usefulness of co_await.
3. If you're using `co_await` for optionals or expected, you cannot simultaneously use `co_await` for waiting on other futures too. That is, if your function returns `future<optional<T>>`, you can't use `co_await` on something that returns an `optional<T>`. The two "channels" of communication can't really coexist within the same function. Having a `co_try` would allow you to make `future<optional<T>>` work.Hmm, this is an interesting point. It reminds me of monad transformers.I suspect co_await on an optional in a coroutine returning future<optional<T>> could be made to work, but the composition would be awkward, i.e. the future's promise would need to know about optional or optional-like things.It's also just wrong. We are using a language feature for purposes it was not intended for before it's even been standardised.If that isn't a screaming alarm bell that this proposed language feature isn't the right design, then I don't know what is.
The dangers and complexities of this "macro" idea are manifold. And by standardizing it as part of the language, we run the risk of creating language-sanctioned chaos in people's code.
1: Macro-functions are not "macros" at all. Calling them is in no way equivalent to macro-expansion; except where otherwise stated, calling a macro function is identical to calling a regular function.
For simplicity's sake, I will continue to refer to them as "macro functions", because there needs to be a distinction between them and regular functions.
2: Macro functions must be inline.
3: Macro functions cannot directly access the scope of their caller. If you need a macro function to access something from the local scope, you pass it as a parameter, just like any other function. This prevents you from accidentally breaking a macro functions just because you named a local variable the same as it expected as part of some interface. If it needs stuff, pass that stuff in.
4: Macro functions cannot leak scope. That is, whatever they declare inside themselves stays inside themselves. It's in its own scope and cannot leak out to the caller's scope. They can return values like any function call, but that's it.
5: You cannot get pointers to macro-functions. So you cannot pass them around. This ensures that the call graph is static.
6: Operations inside a macro function affects the macro function itself, not the caller's scope.
7: Macro functions can only affect the outer scope by issuing control flow changes: `return#`, `break#`, and `continue#`. `continue#` and `break#` cannot be used in places where there is a loop within the scope of a macro function.
8: The "outer scope" of a macro function is the closest non-macro function in the call stack. This allows nested calls to macro-functions.These rules impose a degree of order on this concept. It keeps it from being "stick random code in places where that code can't work" and merely becomes "a function call that can impose some control flow on its caller."
3: Macro functions cannot directly access the scope of their caller. If you need a macro function to access something from the local scope, you pass it as a parameter, just like any other function. This prevents you from accidentally breaking a macro functions just because you named a local variable the same as it expected as part of some interface. If it needs stuff, pass that stuff in.
4: Macro functions cannot leak scope. That is, whatever they declare inside themselves stays inside themselves. It's in its own scope and cannot leak out to the caller's scope. They can return values like any function call, but that's it.
// Macro functions look just like a free function except for the # at the
// end of the function name. Note that the # counts as part of the identifier,
// so return# does not collide with the return keyword.
template<class T> inline int return#(T v)
{
if(v > 0)
return -> v; // control flow keyword + '->' means it affects the
// calling function, not this macro function
if(v < 0)
break ->; // Also this break is executed in the calling function
// We can inject variable declarations into the calling function
// with typename + '->'. This is useful for RAII triggered cleanup.
// Note that __FILE__, __COUNTER__ and __LINE__ refer to the caller,
// not here. This allows injection of uniquely named variables to
// prevent collision
int -> a = 5;
// Otherwise this function macro has a local scope, and code
// executed here remains here
size_t n = 0;
for(; n < 5; n++)
{
// We can also refer to variables in the caller's scope like this
// This lets an initialising macro function call inject state later
// retrievable by a second macro function call
(-> a) ++;
}
// This returns a value from this function macro to the caller
return -1;
}
static int values[]; // some array somewhere
int foo(int n)
{
int acc = 0;
for(;;)
{
// Macro functions are invoked just like normal functions,
// no token pasting. So n will be incremented exactly once.
int ret = return#(values[n++]);
// int a was injected here by the macro function
acc += a;
}
}
3: Macro functions cannot directly access the scope of their caller. If you need a macro function to access something from the local scope, you pass it as a parameter, just like any other function. This prevents you from accidentally breaking a macro functions just because you named a local variable the same as it expected as part of some interface. If it needs stuff, pass that stuff in.
4: Macro functions cannot leak scope. That is, whatever they declare inside themselves stays inside themselves. It's in its own scope and cannot leak out to the caller's scope. They can return values like any function call, but that's it.I've come up with a compromise solution:
// Macro functions look just like a free function except for the # at the
// end of the function name. Note that the # counts as part of the identifier,
// so return# does not collide with the return keyword.
template<class T>
inline int return#(T v)
{
if(v > 0)
return -> v; // control flow keyword + '->' means it affects the
// calling function, not this macro function
if(v < 0)
break ->; // Also this break is executed in the calling function
// If break isn't valid at the point of use
// in the calling function, it will not compile
// We can inject variable declarations into the calling function
// with typename + '->'. This is useful for RAII triggered cleanup
// i.e. these get destructed when the scope of the call point exits.
// Note that the actual name of the variable injected will
// be some very unique identifier which cannot collide with any
// other variable, including those injected by other macro functions
int -> a = 5;
// Otherwise this function macro has a local scope, and code
// executed here remains here
size_t n = 0;
for(; n < 5; n++)
{
// We can also refer to variables previously injected into the
// caller's scope by this macro function like this.
// This lets one keep state across invocations of the macro function
(-> a) ++;
}
// This returns a value from this function macro to the caller
// If you wrote return -> a, that would be a compile error
// as there is no variable called a in this scope.
return (-> a);
}
This paper is much closer to finished than the previous half arsed paper posted here at Vicente's request. It is also pretty much completely rewritten, and Vicente no longer wishes to author it.
T op(X x, Y y) export return U;
U test(X x)
{
T v = op(x, Y{});
U u = transform(v);
return u;
}
template <class T, class E, class T2>
T try_(std::expected<T, E> v) export return std::expected<T2, E>
{
if (v)
return v.value(); // return normally
else
export return v.error(); // return through exported slot
}
expected<float> get_float() noexcept
{
int _int = try_(get_int());
float ret = (float) _int;
if ((int) ret != _int)
return unexpected(std::errc::result_out_of_range);
return ret;
}
Instead of "macro functions" consider the following language extension as an alternative. It appears to me less invasive, more focused, and seems to address your problem.
It looks like it could solve the problem with operator TRY. I do not know if it can address the case of coroutines, as I do not fully understand them.
Instead of "macro functions" consider the following language extension as an alternative. It appears to me less invasive, more focused, and seems to address your problem.This idea looks very similar to Emil's alternative design to Outcome/Expected, https://zajo.github.io/boost-noexcept/. It uses a "side channel" or "out of band" channel for returning alternatives to the normal return.
I'm not fond of OOB based designs for this problem domain. Your proposal I think would also require changes to ABI, specifically mangling. That's a big ask for a fixed function feature with limited reusability.
It looks like it could solve the problem with operator TRY. I do not know if it can address the case of coroutines, as I do not fully understand them.Coroutines as currently proposed require the co_* operators to be called from the coroutinised function. You cannot, for example, call co_await via a helper function without coroutinising the helper function. I find this greatly annoying personally when writing coroutine code. I feel myself reaching for C macros to wrap boilerplate around co_await rather than being bothered to do it properly and write up a new awaitable type just to hide boilerplate.Native C++ macro functions solve that too. They solve a ton of stuff, including lots of problems we don't know about yet.Your proposal is much more fixed featured, and without changes to how a function becomes coroutinised, I don't think could implement co_await/co_yield/co_return. But more experienced folk on here may jump in with alternative opinions on that.
W dniu piątek, 13 października 2017 17:39:24 UTC+2 użytkownik Niall Douglas napisał:Instead of "macro functions" consider the following language extension as an alternative. It appears to me less invasive, more focused, and seems to address your problem.This idea looks very similar to Emil's alternative design to Outcome/Expected, https://zajo.github.io/boost-noexcept/. It uses a "side channel" or "out of band" channel for returning alternatives to the normal return.Emil's library simply uses a thread-local storage. What I propose is just a construct that when compiled generates exactly same code as your macros. The "slot" is only for the purpose of describing the feature, when compiled, it will be replaced with a branch instruction and return.I'm not fond of OOB based designs for this problem domain. Your proposal I think would also require changes to ABI, specifically mangling. That's a big ask for a fixed function feature with limited reusability.I do not think it needs to affect ABI. Similarly to constexpr functions, you can require of functions with exported return slot that their body is visible in places where it is invoked. It is close to a macro, except that it is namespace-scoped and does adhere to name lookup rules of normal functions.
It looks like it could solve the problem with operator TRY. I do not know if it can address the case of coroutines, as I do not fully understand them.Coroutines as currently proposed require the co_* operators to be called from the coroutinised function. You cannot, for example, call co_await via a helper function without coroutinising the helper function. I find this greatly annoying personally when writing coroutine code. I feel myself reaching for C macros to wrap boilerplate around co_await rather than being bothered to do it properly and write up a new awaitable type just to hide boilerplate.Native C++ macro functions solve that too. They solve a ton of stuff, including lots of problems we don't know about yet.Your proposal is much more fixed featured, and without changes to how a function becomes coroutinised, I don't think could implement co_await/co_yield/co_return. But more experienced folk on here may jump in with alternative opinions on that.Now, ere you might be right. I do not know about coroutines that much.
Regards,&rzej;
This idea looks very similar to Emil's alternative design to Outcome/Expected, https://zajo.github.io/boost-noexcept/. It uses a "side channel" or "out of band" channel for returning alternatives to the normal return.Emil's library simply uses a thread-local storage. What I propose is just a construct that when compiled generates exactly same code as your macros. The "slot" is only for the purpose of describing the feature, when compiled, it will be replaced with a branch instruction and return.
I'm not fond of OOB based designs for this problem domain. Your proposal I think would also require changes to ABI, specifically mangling. That's a big ask for a fixed function feature with limited reusability.I do not think it needs to affect ABI. Similarly to constexpr functions, you can require of functions with exported return slot that their body is visible in places where it is invoked. It is close to a macro, except that it is namespace-scoped and does adhere to name lookup rules of normal functions.
Don't feel bad about that; the original "macro" idea wasn't going to replace `co_*` either, for reasons that have been outlined earlier in the thread.
Don't feel bad about that; the original "macro" idea wasn't going to replace `co_*` either, for reasons that have been outlined earlier in the thread.Except your reasons made no sense, as I pointed out.
The paper's current formulation of C++ macro functions follow all the rules for sanity you laid out, and most definitely can replace all the co_* operators entirely.
--
Matthew
This idea looks very similar to Emil's alternative design to Outcome/Expected, https://zajo.github.io/boost-noexcept/. It uses a "side channel" or "out of band" channel for returning alternatives to the normal return.Emil's library simply uses a thread-local storage. What I propose is just a construct that when compiled generates exactly same code as your macros. The "slot" is only for the purpose of describing the feature, when compiled, it will be replaced with a branch instruction and return.My issue was Emil's choice of design pattern i.e. the use of OOB. Not his implementation.
The language should not interact with calls to standard library functions in this fashion. The act of calling a library function should not radically modify a function; if a function's nature is going to be changed, it ought to be changed by an explicit language keyword, not the presence of a mere function call.
The paper's current formulation of C++ macro functions follow all the rules for sanity you laid out, and most definitely can replace all the co_* operators entirely.Your macro idea doesn't follow my sanity rules. They can access the calling function's scope, and their declarations leak into the calling function.
OK, there's been a lot of discussion here, and we seem to have agreement on some points and some variation on others. I'm posting what I think is a summary of where the various discussions are at the present time. Feel free to add corrections if I'm mistaken.For the purposes of this post, I'm going to call these "exporting functions". There seems to be agreement that:1: Exporting functions are functions, not macros. They do not have access to the definitions in their callers, and they do not leak declarations into their callers, with a few very specific exceptions. Compilers can certainly inline them (and may be required to do so, per the stuff below), but they nominally behave the way we expect functions to behave.
2: Exporting functions use special syntax in their prototypes which marks them as being an exporting function.
Here are some things that either are in disagreement or haven't been really talked about much:1: The specific suite of operations that an exporting function can export. There's broad agreement on "return", but nothing much has been discussed about other operations. It should be noted that the use cases outlined in the original paper are solved by just exporting `return`, not more esoteric operations like `continue` and `break`, so `return` may be all we need.
2: Whether the exported operation (including the type being export-returned) is a part of the function's signature. Also, if we require the exported-return type (and we should), we should of course allow placeholders, as for regular return types.3: Should exporting functions be static or dynamic? By "dynamic", I mean can you get pointers to exporting functions and pass them around; can you make virtual exporting functions? Making them "dynamic" means that exporting functions become a first-class part of the compiler's ABI. Obviously if it's part of the ABI, then it must be part of the function's signature, along with the exported return type.To restrict them to just "static" use means restricting them to being inline, visible to all translation units that use them. And of course, not being able to declare them `virtual` or getting function pointers to them in any way.4: Should the invocation of an exporting function have special syntax, to make it visually distinct from calling functions which cannot export operations?5: Should there be special syntax to invoke an exporting function within an exporting function, such that if it exports a return, the return is exported to the outer function's caller? This is somewhat orthogonal to #4, in that even if we don't require a syntax for export return cases, we could still add a syntax to cover this particular case. That is, even if we allow `try(expression)` to implicitly export a return, `export try(expression)` could be used to mean to export the exported return value from the `try(expression)` (resulting in the value of `expression` otherwise). And therefore, `return export try(expression)` would mean to export-return the exported return of `try(expression)`, and return the return result of `try(expression)` if it resulted in a regular value.
On Friday, October 13, 2017 at 8:10:07 PM UTC+1, Nicol Bolas wrote:OK, there's been a lot of discussion here, and we seem to have agreement on some points and some variation on others. I'm posting what I think is a summary of where the various discussions are at the present time. Feel free to add corrections if I'm mistaken.For the purposes of this post, I'm going to call these "exporting functions". There seems to be agreement that:1: Exporting functions are functions, not macros. They do not have access to the definitions in their callers, and they do not leak declarations into their callers, with a few very specific exceptions. Compilers can certainly inline them (and may be required to do so, per the stuff below), but they nominally behave the way we expect functions to behave.Agreed. I've chosen the term "macro functions" but the current formulation is mostly function, little macro.I am semi-tempted to add the 'control flow keyword ->' markup to lambdas, but it's too important that these things get ADL discovery like functions.2: Exporting functions use special syntax in their prototypes which marks them as being an exporting function.Can we please use any adjective other than "export"?Export makes me think of Modules. Or template export, the memory of which makes me queasy.These functions don't export anything. They affect a caller's control flow.
Here are some things that either are in disagreement or haven't been really talked about much:1: The specific suite of operations that an exporting function can export. There's broad agreement on "return", but nothing much has been discussed about other operations. It should be noted that the use cases outlined in the original paper are solved by just exporting `return`, not more esoteric operations like `continue` and `break`, so `return` may be all we need.My macro function have the specific ability to call intrinsics as if they were the calling function.
So, they could call setjmp() and be cast iron guaranteed that the frame stored is that if the caller.
The reason I mention setjmp is because the Coroutines TS only covers half the use cases for coroutines. The other half would enormously benefit from native C++ macro functions. Any coroutines implementation right now is using unwieldy and brittle C macros. Those could be made to permanently go away.
Niall
Can we please use any adjective other than "export"?Export makes me think of Modules. Or template export, the memory of which makes me queasy.These functions don't export anything. They affect a caller's control flow.This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far. And he uses the keyword "export". It's not the best term, but it's more reasonable for the current idea than "macro" or "inline". It's not so much that it's the best word, but it's the least bad thus far.Also, "export" is a verb, so it's possible to talk about a function "exporting" something as a separate action from it "returning" something. If you can come up with a good, single-word term for "causes my caller to return this", feel free.
This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far.
template <class T, class E>
constexpr auto operator try(std::expected<T, E> v) noexcept
{
struct tryer
{
std::expected<T, E> v;
constexpr bool try_return_immediately() const noexcept { return !v.has_value(); }
constexpr auto try_return_value() { return std::move(v).error(); }
constexpr auto try_value() { return std::move(v).value(); }
};
return tryer{ std::move(v) };
}
And he uses the keyword "export". It's not the best term, but it's more reasonable for the current idea than "macro" or "inline". It's not so much that it's the best word, but it's the least bad thus far.Also, "export" is a verb, so it's possible to talk about a function "exporting" something as a separate action from it "returning" something. If you can come up with a good, single-word term for "causes my caller to return this", feel free.
What "coroutines implementations" are you talking about that use C macros? I don't use the Coroutines TS, but I've seen some example code that does. I've yet to see any that needed macros in order to do their jobs.
This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far.I hate to be pedantic, but remember P0779R0 proposes two methods of implementing operator try. The first, to remind you, is this:
template <class T, class E>
constexpr auto operator try(std::expected<T, E> v) noexcept
{
struct tryer
{
std::expected<T, E> v;
constexpr bool try_return_immediately() const noexcept { return !v.has_value(); }
constexpr auto try_return_value() { return std::move(v).error(); }
constexpr auto try_value() { return std::move(v).value(); }
};
return tryer{ std::move(v) };
}If you just want to do "var = try expr" where expr is user definable types and be done with it, the above delivers that.
And he uses the keyword "export". It's not the best term, but it's more reasonable for the current idea than "macro" or "inline". It's not so much that it's the best word, but it's the least bad thus far.Also, "export" is a verb, so it's possible to talk about a function "exporting" something as a separate action from it "returning" something. If you can come up with a good, single-word term for "causes my caller to return this", feel free.Ideally I'd like to see no new keywords and no dual band or second band channelling.
What "coroutines implementations" are you talking about that use C macros? I don't use the Coroutines TS, but I've seen some example code that does. I've yet to see any that needed macros in order to do their jobs.The Coroutines TS only standardises one kind of coroutine. Various experts think about half of coroutine using code can be made to use the Coroutines TS. The rest of coroutines are supposedly coming in a later standards proposal and will require even more injection of boilerplate into calling functions.It's like range for functions. They should have been implemented as a foreach# macro function, not into expanding boilerplate in the language itself.
if (condition)
LOG_WARNING(logger) << "bad condition";
else
process();
#define LOG_WARNING(L) if(L.warning_enabled()) L.stream()
This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far.I hate to be pedantic, but remember P0779R0 proposes two methods of implementing operator try.
The first, to remind you, is this:
template <class T, class E>
constexpr auto operator try(std::expected<T, E> v) noexcept
{
struct tryer
{
std::expected<T, E> v;
constexpr bool try_return_immediately() const noexcept { return !v.has_value(); }
constexpr auto try_return_value() { return std::move(v).error(); }
constexpr auto try_value() { return std::move(v).value(); }
};
return tryer{ std::move(v) };
}If you just want to do "var = try expr" where expr is user definable types and be done with it, the above delivers that.
And he uses the keyword "export". It's not the best term, but it's more reasonable for the current idea than "macro" or "inline". It's not so much that it's the best word, but it's the least bad thus far.Also, "export" is a verb, so it's possible to talk about a function "exporting" something as a separate action from it "returning" something. If you can come up with a good, single-word term for "causes my caller to return this", feel free.Ideally I'd like to see no new keywords and no dual band or second band channelling.
What "coroutines implementations" are you talking about that use C macros? I don't use the Coroutines TS, but I've seen some example code that does. I've yet to see any that needed macros in order to do their jobs.The Coroutines TS only standardises one kind of coroutine. Various experts think about half of coroutine using code can be made to use the Coroutines TS. The rest of coroutines are supposedly coming in a later standards proposal and will require even more injection of boilerplate into calling functions.
On Fri, Oct 13, 2017 at 9:10 PM, Nicol Bolas <jmck...@gmail.com> wrote:> 1: The specific suite of operations that an exporting function can export. There's broad agreement on "return", but nothing much has been discussed about other operations. It should be noted that the use cases outlined in the original paper are solved by just exporting `return`, not more esoteric operations like `continue` and `break`, so `return` may be all we need.The point that the whole proposal is nothing but exceptions in disguise has been beaten to death.So why can't we just say these are indeed exceptions, albeit of a special kind?
If I may express my opinion (and this is just opinion -- I have nothing objective to back it up) I like your first solution best. The scope is clearly defined (only operation `try`), the implementation is clear, and it is also clear that the feature would be easy to use correctly and hard to use incorrectly.
But while the ability to define custom flow control is powerful enough to solve the problems of operation `try`, coroutines, foreach statement and more, it has also power to introduce incomprehensible bugs. This is what I fear when using macros:
if (condition)
LOG_WARNING(logger) << "bad condition";
else
process();if `condition` is false will `process()` be called? That depends on how macro `LOG_WARNING` is defined. If it is defined as:Then I have a bug that will be super-hard to find. And the solution with injecting code (even if it is not a macro) will expose this new kind of bugs for which we are not prepared. I suspect that it will be too easy to use it incorrectly. With my proposal I tried to narrow down the generation of the new control flows to minimum, but I think it is not going to work.
#define LOG_WARNING(L) if(L.warning_enabled()) L.stream()
This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far.I hate to be pedantic, but remember P0779R0 proposes two methods of implementing operator try.Yes, but that's really not what we've been talking about. Discussion in this thread has been focused on "macro-functions" and ideas derived from it, not the "operator try" part.Probably because it's far less interesting of an idea, even if it's more viable of a proposal.
OK, that makes a lot more sense now. To be more specific, what you're talking about is P0534: call/cc.
The thing is, you're wrong that such a coroutine mechanism requires "injection of boilerplate". Call/cc works based on whole function stacks; it does not care how many functions are in the way. If it suspends execution, it suspends execution of the entire call tree down to its root. If it resumes execution, it resumes execution of the entire call tree down to its root.As such, await-style suspend/resume can be done via utility functions/types. Those functions don't have to be "injected" into a specific function's stack.The closest to "injection" that call/cc requires is a feature that it already has: the ability to resume a stack, but doing so by pushing another function on top of that stack (so that you can do some testing/checking/whatever). When that function returns, its value is provided to the suspension code on top of the stack.
> Because that doesn't solve any of the use cases the OP wanted to use them for.I am afraid you misunderstood everything that I wrote. You did that so thoroughly it is astonishing.
This is getting into bikeshedding, but I picked "export" because Andrzej's post represents the most fully formed post on this thread that represents our current thinking thus far.I hate to be pedantic, but remember P0779R0 proposes two methods of implementing operator try.Yes, but that's really not what we've been talking about. Discussion in this thread has been focused on "macro-functions" and ideas derived from it, not the "operator try" part.Probably because it's far less interesting of an idea, even if it's more viable of a proposal.Dunno, JF thinks it's dead on arrival without lots more proof of its need. See https://groups.google.com/a/isocpp.org/d/msg/sg14/6L5h2OKg75U/UDcY_ZgGAwAJ
OK, that makes a lot more sense now. To be more specific, what you're talking about is P0534: call/cc.Actually, I'm not. Oliver who proposed that is a fellow Booster. We know each other. That proposal was one of many of a rearguard action by domain experts in coroutines to dissuade WG21 of adopting the Coroutines TS due to it being inferior to library based alternatives around a very minimal compiler intrinsics API. Indeed I may have commented on the design of that API at one point. The dissuasion failed, some would feel for political not technical reasons.
But little birdies tell me that work is ongoing to complement the Coroutines TS with something "more useful" (not my words) by filling in the gaps.
That's not my endeavour, my interaction with that group has mainly been to coordinate the coroutinised i/o in AFIO with what they're planning, and even with that it is mostly to tell them that coroutinised i/o is pointless, and not useful on modern storage. Still, AFIO supports it for those who want it, with a big warning sign in the docs saying "don't use this, it's slower".The thing is, you're wrong that such a coroutine mechanism requires "injection of boilerplate". Call/cc works based on whole function stacks; it does not care how many functions are in the way. If it suspends execution, it suspends execution of the entire call tree down to its root. If it resumes execution, it resumes execution of the entire call tree down to its root.As such, await-style suspend/resume can be done via utility functions/types. Those functions don't have to be "injected" into a specific function's stack.The closest to "injection" that call/cc requires is a feature that it already has: the ability to resume a stack, but doing so by pushing another function on top of that stack (so that you can do some testing/checking/whatever). When that function returns, its value is provided to the suspension code on top of the stack.All of what you just said would be disproved by the several coroutines implementations in Boost. They all use C macros or template based mechanisms of injecting boilerplate. Extensively.
You may also find https://github.com/jamboree/co2 of interest. It's a library and C macro implementation of the Coroutines TS. No language changes needed.
If we had the native C++ macro functions I proposed, it actually could entirely replace the Coroutines TS entirely.
V.
The final edition has a couple claims about coroutines that I don't think are true:
- "the function’s return type must be an awaitable". Not if the function's callers don't co_await on it.
- "you cannot await on something different to the coroutine return type". This restriction, if it were true, would hamper both the async and generator use cases.