float select(int i, float a, float b, float c) {
switch (i) {
case 0:
return a;
case 1:
return b;
case 2:
return c;
}
}
float *p;
if (p && *p > 0) { ... } // this is valid
bool AND(bool a, bool b) {
return a && b;
}
if (AND(p, *p > 0)) { ... } // This is not
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return a;
case 1:
return b;
case 2:
return c;
}
}
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return @a;
case 1:
return @b;
case 2:
return @c;
}
}
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return a();
case 1:
return b();
case 2:
return c();
}
}
float select(int i, std::expression<float>
a, std::expression<float>
b, std::expression<float>
c) {
switch (i) {
case 0:
return a();
case 1:
return b();
case 2:
return c();
}
}
float* p;
int i;
float r = select(i, 1.f, *p, std::sqrt(*p));
// equivalent to:
float r;
switch (i) {
case 0:
r = 1.f;
break;
case 1:
r = *p;
break;
case 2:
r = std::sqrt(*p);
break;
}
Hello everyone,
I would want to propose a new way to pass parameters to functions.
Currently, we have 2 way to pass parameters: by value, and by reference.
(Passing by pointer is really passing by value the address)
However, none of those allow delayed evaluation of parameters.
--
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-proposal...@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/7fb13c92-2d15-44a0-8aa1-848539f8658d%40isocpp.org.
Lazy arguments are compatible with terse lambda. Having one will
not remove the need for the other.
You seem to miss one crucial point with this proposal (P0927): it
will not create anonymous classes at all: it will always use a
function pointer like this: T (*)(void*)
If I give an example with the proposal syntax, this will be:
int foo([] -> int i) {
return i();
}
int bar(int j) {
return foo(j+2);
}
int foobar(int j) {
return foo(2*j);
}
int foo(int (*i)(void*), void* stack_p) {
return i(stack_p);
}
int bar(int j) {
int closure(void* stack_p) {
int j = *static_cast<int*>(stack_p + /* offset j
*/);
return j+2;
}
return foo(&closure,
__builtin_frame_address(0));
}
int foobar(int j) {
int closure(void* stack_p) {
int j = *static_cast<int*>(stack_p
+ /*
offset j */);
return 2*j;
}
return foo(&closure,
__builtin_frame_address(0));
}
int bar([] -> int i) { return i(); }
int baz(std::function_ref<int()> f) { return f(); }
int foo([] -> int i) {
return bar(i()) + baz([&] { return i(); };
}
int bar([] -> int i) { return i; }
int baz(std::function_ref<int()> f) { return f(); }
int foo([] -> int i) {
return bar(i) + baz([&] { return i; };
}
int* foo([] -> int& i) { return &i; } // OK
int* bar([] -> int i) { return &i; } // ill-formed, taking address of a prvalue
On Tuesday, May 29, 2018 at 5:09:43 PM UTC+3, Edward Catmur wrote:
Question: Why would you ever want to use [] -> int instead of std::function_ref<int()> as a parameter?
Question: Why would you ever want to use [] -> int instead of std::function_ref<int()> as a parameter?
- On the call site I really doubt not be able to see you are NOT constructing the argument on function call is a good idea
On Tue, May 29, 2018 at 3:36 PM, <mihailn...@gmail.com> wrote:
On Tuesday, May 29, 2018 at 5:09:43 PM UTC+3, Edward Catmur wrote:
...
Question: Why would you ever want to use [] -> int instead of std::function_ref<int()> as a parameter?
[] -> int is transparent at the call site; std::function_ref<int()> requires the calling code to write a lambda. It is also lighter weight.
func([]() -> int{...}());
If we're providing the possibility of unevaluated parameters, with the functionin charge of whether they're evaluated, it seems valuable to let the evaluation
look deliberate. It also sidesteps the question of whether an unevaluated
parameter can be evaluated at most once or more than once, by making
evaluations manifest in the code.
The worst case would be to to have the compiler decide behind the scenes
whether to do the evaluation based on its detection of the first use of the
parameter.
On Tue, May 29, 2018 at 4:00 PM, Hyman Rosen <hyman...@gmail.com> wrote:If we're providing the possibility of unevaluated parameters, with the functionin charge of whether they're evaluated, it seems valuable to let the evaluation
look deliberate. It also sidesteps the question of whether an unevaluated
parameter can be evaluated at most once or more than once, by making
evaluations manifest in the code.
The worst case would be to to have the compiler decide behind the scenes
whether to do the evaluation based on its detection of the first use of the
parameter.Thanks, I think those are pretty strong arguments in favor of `identifier()` as the invocation syntax and against bare `identifier`.
But I don't like the idea of allowing the user to use `identifier` as if it were a function or functor.
Or even as if it were an object. It should be a completely different order of things, fundamentally
distinct from anything else in C++. Much like braced-init-lists aren't expressions, captured
expressions identifiers should not be objects or functions.
On Tuesday, May 29, 2018 at 10:53:45 AM UTC-4, mihailn...@gmail.com wrote:
On Tuesday, May 29, 2018 at 5:44:34 PM UTC+3, Edward Catmur wrote:On Tue, May 29, 2018 at 3:36 PM, <mihailn...@gmail.com> wrote:
On Tuesday, May 29, 2018 at 5:09:43 PM UTC+3, Edward Catmur wrote:...Question: Why would you ever want to use [] -> int instead of std::function_ref<int()> as a parameter?[] -> int is transparent at the call site; std::function_ref<int()> requires the calling code to write a lambda. It is also lighter weight.What if the user decides to upgrade the call and write a lambda? What if the user wants to pass a callable instance variable? Overloads?What happens when a user decides to stop using `std::string` and start trying to pass a `std::vector<char>`? They get a compile error for not providing the thing they were told by the API to provide.
How is that any different? You said "give me an expression that results in an integer". You instead tries to give a lambda. That's not "an expression that results in an integer", so you get a compile error.You can turn any function-that-results-in-an-integer into an expresion-that-results-in-an-integer simply by calling it in-situ:
template <class T>
T foo(std::function_ref<T()> f) { return f(); }
foo(1) // ok automatic conversion from 1 to std::function<int()>: lazy evaluation
foo(std::function_ref<int()>([](){ return 1; })) // std::function_ref<int()> is copied, not lazy evaluated
On Tuesday, May 29, 2018 at 6:03:29 PM UTC+3, Nicol Bolas wrote:What happens when a user decides to stop using `std::string` and start trying to pass a `std::vector<char>`? They get a compile error for not providing the thing they were told by the API to provide.And that is exactly why we are moving away from std::string as a param to string_view!You want a good, general interface. Accepting a lazy arg alone is not one.
How is that any different? You said "give me an expression that results in an integer". You instead tries to give a lambda. That's not "an expression that results in an integer", so you get a compile error.You can turn any function-that-results-in-an-integer into an expresion-that-results-in-an-integer simply by calling it in-situ:And both bind to function_ref hence function_ref is the better interface.
The problem is, the user knows the code is late evaluated - he has seen the declaration.Second, for the user there is no practical difference b/w "function-that-results-in-an-integer" and "expresion-that-results-in-an-integer"Because of this the user will want to pass callables as well.
With lazy arg interface alone this will be both clumsy and awkward (to say the least).
func([]() -> int{...}());That makes it clear to everyone that the function call is the expression being captured, not the function itself.
--
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/5oUZysJB4HE/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/4a0155aa-490d-4948-8ade-732c339e1e8d%40isocpp.org.
On Tuesday, May 29, 2018 at 6:03:29 PM UTC+3, Nicol Bolas wrote:
On Tuesday, May 29, 2018 at 10:53:45 AM UTC-4, mihailn...@gmail.com wrote:
On Tuesday, May 29, 2018 at 5:44:34 PM UTC+3, Edward Catmur wrote:On Tue, May 29, 2018 at 3:36 PM, <mihailn...@gmail.com> wrote:
On Tuesday, May 29, 2018 at 5:09:43 PM UTC+3, Edward Catmur wrote:...Question: Why would you ever want to use [] -> int instead of std::function_ref<int()> as a parameter?[] -> int is transparent at the call site; std::function_ref<int()> requires the calling code to write a lambda. It is also lighter weight.What if the user decides to upgrade the call and write a lambda? What if the user wants to pass a callable instance variable? Overloads?What happens when a user decides to stop using `std::string` and start trying to pass a `std::vector<char>`? They get a compile error for not providing the thing they were told by the API to provide.And that is exactly why we are moving away from std::string as a param to string_view!
On Tue, May 29, 2018 at 5:12 PM, <mihailn...@gmail.com> wrote:On Tuesday, May 29, 2018 at 6:03:29 PM UTC+3, Nicol Bolas wrote:What happens when a user decides to stop using `std::string` and start trying to pass a `std::vector<char>`? They get a compile error for not providing the thing they were told by the API to provide.And that is exactly why we are moving away from std::string as a param to string_view!You want a good, general interface. Accepting a lazy arg alone is not one.std::string_view is strictly better than std::string as a parameter; it has a smaller footprint than a std::string passed by value, and it requires less indirection than a std::string passed by reference. In addition, the relevant template is instantiated a limited number of times: at most once for basic_string_view<char> per translation unit.Contra this, std::function_ref must be instantiated once per function type and its constructor must be instantiated once per callable type - once per call, if the callables are closures. Its footprint (two pointers) is at least as large as either a closure type (0-1 pointers, for a capture-by-reference default) or a lazy parameter (1-2 pointers, depending on implementation) and requires a closure to be reified which might otherwise not occur. Its call pointer may result in double-indirection if not inlined, and bloats the symbol table, particularly if debug symbols are enabled. Again, if debugging is enabled or if inlining fails, its call operator pollutes the call stack with irrelevant frames.How is that any different? You said "give me an expression that results in an integer". You instead tries to give a lambda. That's not "an expression that results in an integer", so you get a compile error.You can turn any function-that-results-in-an-integer into an expresion-that-results-in-an-integer simply by calling it in-situ:And both bind to function_ref hence function_ref is the better interface.Using function_ref requires wrapping an expression in a closure - `expr` becomes at minimum `[&] { return expr; }` or quite possibly `[&]() -> decltype(auto) { return expr; }` - a large amount of unnecessary boilerplate. Passing a callable as a lazy parameter requires appending a pair of parentheses.The problem is, the user knows the code is late evaluated - he has seen the declaration.Second, for the user there is no practical difference b/w "function-that-results-in-an-integer" and "expresion-that-results-in-an-integer"Because of this the user will want to pass callables as well.Why? How likely are they have callables lying around with precisely the semantics that are required?With lazy arg interface alone this will be both clumsy and awkward (to say the least).Two characters `()` is clumsy and awkward?
func([]() -> int{...}());That makes it clear to everyone that the function call is the expression being captured, not the function itself.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.
foo(bar(baz()));
Ok, give me a sales pitch. When to choose func_ref and when lazy if I what to present my class to the world.
On one hand is func_ref which binds to everything but must introduce an explicit lambda to write code inline, on the other is lazy, in which inline code is the default, but is single single expression only and does not bind to anything (inline only).For me, personally func_ref is the better interface as it is the most general, but I am obviously missing something (performance aside) as you all are so exited.When I will want to use lazy? What are the rules of thumb? What is "the killer app"?
new(container.get_mem_for_new_item()) auto(expr);
@florian: Yes, good point: The fact that you don't need a "already evaluated" flag for simple expressions clearly points to the fact that the caller side should handle the flag, i.e. the callee should just call the function in case it needs the value. The function can elect to use a flag or re-evalutate if it can figure out that runtime cost is low, no side effects occur and no temporary destructors to call. Using thunks to adjust stack frame offsets is probably wise as it is going to be fairly rare that forwarding occurs (especially in multiple levels).@Nicol: Prohibiting using a lazy parameter more than once seems overly restrictive and very error prone, especially if no diagnostic is required. Furthermore the caller must keep track of whether the function was called anyway to handle temporary destruction properly.
Den tisdag 29 maj 2018 kl. 22:25:16 UTC+2 skrev Nicol Bolas:On Tuesday, May 29, 2018 at 4:21:21 PM UTC-4, Nicol Bolas wrote:On Tuesday, May 29, 2018 at 1:47:45 PM UTC-4, mihailn...@gmail.com wrote:On one hand is func_ref which binds to everything but must introduce an explicit lambda to write code inline, on the other is lazy, in which inline code is the default, but is single single expression only and does not bind to anything (inline only).For me, personally func_ref is the better interface as it is the most general, but I am obviously missing something (performance aside) as you all are so exited.When I will want to use lazy? What are the rules of thumb? What is "the killer app"?The two examples I gave above are the "killer app". Transparent lazy evaluation equivalent to C++'s `&&` and `||` behavior, and the ability to have `lazy_emplace` work. Neither of these cases are things for which passing a function is the natural interface.The rule of thumb is that you use a lazy expression when it makes sense based on what you're doing. `lazy_emplace` does it because it is the most natural way to make it work. The natural low-level user code is:
new(container.get_mem_for_new_item()) auto(expr);So we invert that by having `container.lazy_emplace` apply `expr` internally. Same thing, just done without having to expose the user to the low-level guts. And since `expr` could throw exceptions, those now happen within the purview of `container`'s member functions, which can abort the object creation properly.Some other rules of thumb:1. You can't affect how lazy expressions evaluate. That is, you can't give them values; they are simply a thing that generates a value. That's another reason `std::sort` and most algorithms wouldn't use them.2. You can't evaluate them more than once. UB ought to result if you try.3. You can't pass a lazy expression outside of your call stack. Nor can you return one.So if you ever need to do any of these things, you are clearly using the wrong tool.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/b5a52d3d-3255-41b8-8649-ce4703f1ac2e%40isocpp.org.
On Tue, 29 May 2018, 23:03 Bengt Gustafsson, <bengt.gu...@beamways.com> wrote:@florian: Yes, good point: The fact that you don't need a "already evaluated" flag for simple expressions clearly points to the fact that the caller side should handle the flag, i.e. the callee should just call the function in case it needs the value. The function can elect to use a flag or re-evalutate if it can figure out that runtime cost is low, no side effects occur and no temporary destructors to call. Using thunks to adjust stack frame offsets is probably wise as it is going to be fairly rare that forwarding occurs (especially in multiple levels).@Nicol: Prohibiting using a lazy parameter more than once seems overly restrictive and very error prone, especially if no diagnostic is required. Furthermore the caller must keep track of whether the function was called anyway to handle temporary destruction properly.In the proposal under discussion, there is no caching of the result of the evaluation of the passed in expression and so no flag required and no destructor call. Multiple use results in multiple evaluation of the initializer expression, which may result in UB if the evaluation of the expression falsifies its own preconditions (e.g. moving out a container or smart pointer that is required to be non empty).
On Tue, 29 May 2018, 23:03 Bengt Gustafsson, <bengt.gu...@beamways.com> wrote:@florian: Yes, good point: The fact that you don't need a "already evaluated" flag for simple expressions clearly points to the fact that the caller side should handle the flag, i.e. the callee should just call the function in case it needs the value. The function can elect to use a flag or re-evalutate if it can figure out that runtime cost is low, no side effects occur and no temporary destructors to call. Using thunks to adjust stack frame offsets is probably wise as it is going to be fairly rare that forwarding occurs (especially in multiple levels).@Nicol: Prohibiting using a lazy parameter more than once seems overly restrictive and very error prone, especially if no diagnostic is required. Furthermore the caller must keep track of whether the function was called anyway to handle temporary destruction properly.In the proposal under discussion, there is no caching of the result of the evaluation of the passed in expression and so no flag required and no destructor call. Multiple use results in multiple evaluation of the initializer expression, which may result in UB if the evaluation of the expression falsifies its own preconditions (e.g. moving out a container or smart pointer that is required to be non empty).
void print(std::string_view sv) { std::cout << sv << std::endl; }
void print_lazy([] -> std::string_view sv) { std::cout << sv() << std::endl; }
void foo0() {
print(std::string("foo")); // ok: temporary string is kept alive until after the execution of print
}
void foo1() {
print_lazy(std::string("foo")); // std::string is generated within "evaluation function" and cannot be kept alive until after the execution of print_lazy
}
void foo2() {
print([]() -> std::string_view { return std::string("foo"); }) // same issue as above, but made explicit for better understanding
}
void print_lazy(std::string_view (*sv_expr)(void*), void* stack_p) {
std::cout << sv_expr(stack_p) << std::endl; // no lifetime management here
}
void foo1() {
// lazy parameter storage
bool __lazy_executed = false;
alignas(alignof(std::string)) std::byte __lazy_str_storage[sizeof(std::string)];
// lazy parameter executor
std::string_view __lazy(void* stack_p) {
std::string* p = new(stack_p + /* __lazy_str_storage offset */) std::string("foo");
// ensure the string is deleted if std::string_view construction throws
auto str_deleter = std::make_scope_exit([p]{ p->~std::string(); })
std::string_view sv = std::string_view(*p); // no exception guarantee for this constructor
// if creation of the string throws, the object is not created and don't need to be destroyed
// The flag will not be set in that case as the exception is not caught
*static_cast<bool*>(stack_p + /* __lazy_executed offset */) = true;
return sv; // "NRVO" is mandatory here
}
// call to print_lazy
print_lazy(&__lazy, __builtin_stack_address(0));
// destroy the temporary string:
if (__lazy_executed) {
static_cast<std::string*>(__lazy_str_storage)->~std::string();
}
}
void print_lazy(std::string_view (*sv_expr)(void*), void* stack_p) {
std::cout << sv_expr(stack_p) << std::endl; // no lifetime management here
}
void foo1() {
// lazy parameter storage
bool __lazy_executed = false;
alignas(alignof(std::string)) std::byte __lazy_str_storage[sizeof(std::string)];
// lazy parameter executor
std::string_view __lazy(void* stack_p) {
std::string* p = new(stack_p + /* __lazy_str_storage offset */) std::string("foo");
// ensure the string is deleted if std::string_view construction throws
try {
std::string_view sv = std::string_view(*p); // no exception guarantee for this constructor
} catch (...) {
p->~std::string();
throw;
}
Le mercredi 30 mai 2018 01:22:11 UTC+2, Edward Catmur a écrit :On Tue, 29 May 2018, 23:03 Bengt Gustafsson, <bengt.gu...@beamways.com> wrote:@florian: Yes, good point: The fact that you don't need a "already evaluated" flag for simple expressions clearly points to the fact that the caller side should handle the flag, i.e. the callee should just call the function in case it needs the value. The function can elect to use a flag or re-evalutate if it can figure out that runtime cost is low, no side effects occur and no temporary destructors to call. Using thunks to adjust stack frame offsets is probably wise as it is going to be fairly rare that forwarding occurs (especially in multiple levels).@Nicol: Prohibiting using a lazy parameter more than once seems overly restrictive and very error prone, especially if no diagnostic is required. Furthermore the caller must keep track of whether the function was called anyway to handle temporary destruction properly.In the proposal under discussion, there is no caching of the result of the evaluation of the passed in expression and so no flag required and no destructor call. Multiple use results in multiple evaluation of the initializer expression, which may result in UB if the evaluation of the expression falsifies its own preconditions (e.g. moving out a container or smart pointer that is required to be non empty).The problem here is not caching the result. If you want to evaluate the expression multiple times, then passing an actual callable object would be preferable. So caching is not needed.The real problem is temporaries lifetime. Let me give you an example:
void print(std::string_view sv) { std::cout << sv << std::endl; }
void print_lazy([] -> std::string_view sv) { std::cout << sv() << std::endl; }
void foo0() {
print(std::string("foo")); // ok: temporary string is kept alive until after the execution of print
}
void foo1() {
print_lazy(std::string("foo")); // std::string is generated within "evaluation function" and cannot be kept alive until after the execution of print_lazy
}
void foo2() {
print([]() -> std::string_view { return std::string("foo"); }) // same issue as above, but made explicit for better understanding
}
If temporaries are destroyed by the "evaluation function", print and print_lazy cannot be semantically equivalent, and lazy parameters would bring many lifetime issues that are not desirable.If foo0 is correct (and it is), then foo1 should also be correct, but this cannot be if temporary lifetime is bound to the "evaluation function"The idea was the temporaries use the caller storage (in the caller stack frame), and are destroyed by the caller after the callee execution.Here we would need to store a flag for the caller to know if the temporaries need to be destroyed or not.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/e79281d1-55a4-41f0-8f42-95050c8ec679%40isocpp.org.
Le mercredi 30 mai 2018 01:22:11 UTC+2, Edward Catmur a écrit :On Tue, 29 May 2018, 23:03 Bengt Gustafsson, <bengt.gu...@beamways.com> wrote:@florian: Yes, good point: The fact that you don't need a "already evaluated" flag for simple expressions clearly points to the fact that the caller side should handle the flag, i.e. the callee should just call the function in case it needs the value. The function can elect to use a flag or re-evalutate if it can figure out that runtime cost is low, no side effects occur and no temporary destructors to call. Using thunks to adjust stack frame offsets is probably wise as it is going to be fairly rare that forwarding occurs (especially in multiple levels).@Nicol: Prohibiting using a lazy parameter more than once seems overly restrictive and very error prone, especially if no diagnostic is required. Furthermore the caller must keep track of whether the function was called anyway to handle temporary destruction properly.In the proposal under discussion, there is no caching of the result of the evaluation of the passed in expression and so no flag required and no destructor call. Multiple use results in multiple evaluation of the initializer expression, which may result in UB if the evaluation of the expression falsifies its own preconditions (e.g. moving out a container or smart pointer that is required to be non empty).The problem here is not caching the result. If you want to evaluate the expression multiple times, then passing an actual callable object would be preferable. So caching is not needed.The real problem is temporaries lifetime. Let me give you an example:
void print(std::string_view sv) { std::cout << sv << std::endl; }
void print_lazy([] -> std::string_view sv) { std::cout << sv() << std::endl; }
void foo0() {
print(std::string("foo")); // ok: temporary string is kept alive until after the execution of print
}
void foo1() {
print_lazy(std::string("foo")); // std::string is generated within "evaluation function" and cannot be kept alive until after the execution of print_lazy
}
void foo2() {
print([]() -> std::string_view { return std::string("foo"); }) // same issue as above, but made explicit for better understanding
}
If temporaries are destroyed by the "evaluation function", print and print_lazy cannot be semantically equivalent, and lazy parameters would bring many lifetime issues that are not desirable.If foo0 is correct (and it is), then foo1 should also be correct, but this cannot be if temporary lifetime is bound to the "evaluation function"
Functions that use lazily evaluated expressions are not intended to be equivalent to their non-lazy counterparts. That's why we give them lazily evaluated expressions; we don't want them to be equivalent.Yes, temporaries manifested in a lazy expression will not be extended to the lifetime in which the original expression was passed. This is a good thing. In your example, there is no lifetime issue at all. Temporaries generated by `print_lazy`'s lazy evaluation will be destroyed after `std::cout` is executed, in accord with C++'s usual lifetime rules.This is a great example of why I say we shouldn't think of lazy evaluation as a function call. Because it isn't a function call and shouldn't act like one. A normal C++ function cannot spawn a temporary whose lifetime is extended to the end of an expression.A lazy expression must be able to do this. Evaluating one ought to be exactly identical to doing a copy-and-paste of the expression at the point of evaluation. Otherwise, there's just no point in bothering.
On Wed, May 30, 2018 at 2:29 PM, Nicol Bolas <jmck...@gmail.com> wrote:Functions that use lazily evaluated expressions are not intended to be equivalent to their non-lazy counterparts. That's why we give them lazily evaluated expressions; we don't want them to be equivalent.Yes, temporaries manifested in a lazy expression will not be extended to the lifetime in which the original expression was passed. This is a good thing. In your example, there is no lifetime issue at all. Temporaries generated by `print_lazy`'s lazy evaluation will be destroyed after `std::cout` is executed, in accord with C++'s usual lifetime rules.This is a great example of why I say we shouldn't think of lazy evaluation as a function call. Because it isn't a function call and shouldn't act like one. A normal C++ function cannot spawn a temporary whose lifetime is extended to the end of an expression.A lazy expression must be able to do this. Evaluating one ought to be exactly identical to doing a copy-and-paste of the expression at the point of evaluation. Otherwise, there's just no point in bothering.OK, so you're saying that invoid print_lazy([] -> std::string_view sv) { std::cout << sv(); std::cout << std::endl; }// ^ #1 ^ #2void foo1() { print_lazy(std::string("foo")); }the temporary std::string should be destructed at #1, not at #2.
I agree that that would be a better semantic, but it implies a different ABI to that proposed (in passing) in P0927; rather than a single code pointer code whose invocation evaluates the expression, we need two code pointers with the second destructing temporaries, or a single code pointer with signature T(void* caller_stack, enum class action { evaluate, destruct_temporaries}). The caller would still need to reserve stack space for any temporaries in the lazily evaluated expression, but would not need to maintain a flag indicating whether temporaries had been constructed, as calling the code pointer to perform cleanup would be the responsibility of the callee.
On Wednesday, May 30, 2018 at 10:14:02 AM UTC-4, Edward Catmur wrote:On Wed, May 30, 2018 at 2:29 PM, Nicol Bolas <jmck...@gmail.com> wrote:Functions that use lazily evaluated expressions are not intended to be equivalent to their non-lazy counterparts. That's why we give them lazily evaluated expressions; we don't want them to be equivalent.Yes, temporaries manifested in a lazy expression will not be extended to the lifetime in which the original expression was passed. This is a good thing. In your example, there is no lifetime issue at all. Temporaries generated by `print_lazy`'s lazy evaluation will be destroyed after `std::cout` is executed, in accord with C++'s usual lifetime rules.This is a great example of why I say we shouldn't think of lazy evaluation as a function call. Because it isn't a function call and shouldn't act like one. A normal C++ function cannot spawn a temporary whose lifetime is extended to the end of an expression.A lazy expression must be able to do this. Evaluating one ought to be exactly identical to doing a copy-and-paste of the expression at the point of evaluation. Otherwise, there's just no point in bothering.OK, so you're saying that invoid print_lazy([] -> std::string_view sv) { std::cout << sv(); std::cout << std::endl; }// ^ #1 ^ #2void foo1() { print_lazy(std::string("foo")); }the temporary std::string should be destructed at #1, not at #2.Yes.
I agree that that would be a better semantic, but it implies a different ABI to that proposed (in passing) in P0927; rather than a single code pointer code whose invocation evaluates the expression, we need two code pointers with the second destructing temporaries, or a single code pointer with signature T(void* caller_stack, enum class action { evaluate, destruct_temporaries}). The caller would still need to reserve stack space for any temporaries in the lazily evaluated expression, but would not need to maintain a flag indicating whether temporaries had been constructed, as calling the code pointer to perform cleanup would be the responsibility of the callee.ABI is essentially irrelevant, because the only way you can have lazy expressions work correctly is if the function(s) that uses them are inlined, relative to the function that provided the expression.
The only cross-ABI way to make lazy expressions work is to make them an actual function, with the expected C++ semantics. At which point... they're not lazy expressions anymore. They're just a transparent way to turn an expression into a (lighter-weight) lambda.
Basically, functions that capture lazy expressions have to be treated like template functions to some degree.
I have the impression you misunderstood my point, so let me rephrase it.
In my example, there is 3 expressions we need to consider:
print_lazy(std::string("foo")): call expression from the caller
std::string("foo"): lazy expression defined in the caller, executed in the callee
std::cout << sv_expr() << std::endl: expression in the callee triggering the execution of the lazy expression
To be fair, the lazy expression returns a std::string_view, so should be implicitly transformed into std::string_view(std::string("foo"))
Now my point is, the temporary std::string object created by the lazy expression must outlive the callee expression (std::cout << sv_expr() << std::endl).
But if you follow naively lifetime rules, this temporary will be destroyed after the std::string_view is created and before std::cout << sv_expr() is executed.
And the program will try to access unallocated data.This is the issue I pointed out and that need to be solved.If a proposal makes this code undefined behavior, then the whole feature is broken from the beginning.Actually, I even think the lazy expression should outlive the whole execution of the caller expression (print_lazy(std::string("foo"))).This is what we expect from a regular expression, I don't see why it should be different with lazy expressions.
I agree that that would be a better semantic, but it implies a different ABI to that proposed (in passing) in P0927; rather than a single code pointer code whose invocation evaluates the expression, we need two code pointers with the second destructing temporaries, or a single code pointer with signature T(void* caller_stack, enum class action { evaluate, destruct_temporaries}). The caller would still need to reserve stack space for any temporaries in the lazily evaluated expression, but would not need to maintain a flag indicating whether temporaries had been constructed, as calling the code pointer to perform cleanup would be the responsibility of the callee.ABI is essentially irrelevant, because the only way you can have lazy expressions work correctly is if the function(s) that uses them are inlined, relative to the function that provided the expression.That's not true, there are many ways to implement this to have the correct behavior without inlining. Edward's solution is one.
And as I said before, I don't what you call "the correct behavior" is what we want.The only cross-ABI way to make lazy expressions work is to make them an actual function, with the expected C++ semantics. At which point... they're not lazy expressions anymore. They're just a transparent way to turn an expression into a (lighter-weight) lambda.Yes, so what? C++ is ABI agnostic, you repeat this often enough. And now you want to specify language constructions to solve an implementation/ABI issue?
Remember that the concept of inlining itself is not part of C++.
On Wednesday, May 30, 2018 at 10:46:39 AM UTC-4, floria...@gmail.com wrote:I agree that that would be a better semantic, but it implies a different ABI to that proposed (in passing) in P0927; rather than a single code pointer code whose invocation evaluates the expression, we need two code pointers with the second destructing temporaries, or a single code pointer with signature T(void* caller_stack, enum class action { evaluate, destruct_temporaries}). The caller would still need to reserve stack space for any temporaries in the lazily evaluated expression, but would not need to maintain a flag indicating whether temporaries had been constructed, as calling the code pointer to perform cleanup would be the responsibility of the callee.ABI is essentially irrelevant, because the only way you can have lazy expressions work correctly is if the function(s) that uses them are inlined, relative to the function that provided the expression.That's not true, there are many ways to implement this to have the correct behavior without inlining. Edward's solution is one.And as I said before, I don't what you call "the correct behavior" is what we want.The only cross-ABI way to make lazy expressions work is to make them an actual function, with the expected C++ semantics. At which point... they're not lazy expressions anymore. They're just a transparent way to turn an expression into a (lighter-weight) lambda.Yes, so what? C++ is ABI agnostic, you repeat this often enough. And now you want to specify language constructions to solve an implementation/ABI issue?No, I want to specify lazy expressions so that they're expressions, not function calls. So that they work exactly as if you had copy-and-pasted the expression at the site of use.
On Wednesday, May 30, 2018 at 10:46:39 AM UTC-4, floria...@gmail.com wrote:I have the impression you misunderstood my point, so let me rephrase it.
In my example, there is 3 expressions we need to consider:
print_lazy(std::string("foo")): call expression from the caller
std::string("foo"): lazy expression defined in the caller, executed in the callee
std::cout << sv_expr() << std::endl: expression in the callee triggering the execution of the lazy expression
To be fair, the lazy expression returns a std::string_view, so should be implicitly transformed into std::string_view(std::string("foo"))
Now my point is, the temporary std::string object created by the lazy expression must outlive the callee expression (std::cout << sv_expr() << std::endl).Why "must" it? Just because it would have if the expression weren't lazily evaluated?
But if you follow naively lifetime rules, this temporary will be destroyed after the std::string_view is created and before std::cout << sv_expr() is executed.That's only true if you think of `sv_expr()` as a function call.If you think of it as being the exact equivalent to `std::cout << std::string_view(std::string("foo"));`, then it works just fine. Evaluating that expression will manifest a temporary. And per C++ rules, by the way that temporary is used within the evaluated expression, the lifetime of that temporary will be the lifetime of the whole expression in which it is used. And the "whole expression" includes `std::cout <<`.This is why thinking of lazy expressions as functions is a bad thing. They aren't functions; they don't act like functions, and they can do things functions cannot normally do.
And the program will try to access unallocated data.This is the issue I pointed out and that need to be solved.If a proposal makes this code undefined behavior, then the whole feature is broken from the beginning.Actually, I even think the lazy expression should outlive the whole execution of the caller expression (print_lazy(std::string("foo"))).This is what we expect from a regular expression, I don't see why it should be different with lazy expressions.I don't see why we should expect lazy expressions to behave exactly like non-lazy ones.
void print(std::string_view sv) {
std::string_view sv2 = sv;
std::cout << sv2 << std::endl;
std::cerr << sv2 << std::endl;
}
void print_lazy([] -> std::string_view sv_expr) {
std::string_view sv2 = sv();
std::cout << sv2 << std::endl;
std::cerr << sv2 << std::endl;
}
void foo0() { print(std::string("foo")); } // definitely correct
void foo1() { print_lazy(std::string("foo")); } // Is this correct?
void foo2() { print([] -> std::string_view { return std::string("foo"); }()); } // not correct
...
The only cross-ABI way to make lazy expressions work is to make them an actual function, with the expected C++ semantics. At which point... they're not lazy expressions anymore. They're just a transparent way to turn an expression into a (lighter-weight) lambda.Yes, so what? C++ is ABI agnostic, you repeat this often enough. And now you want to specify language constructions to solve an implementation/ABI issue?
No, I want to specify lazy expressions so that they're expressions, not function calls. So that they work exactly as if you had copy-and-pasted the expression at the site of use.
The only way to do that is to allow the compiler to "instantiate" every function that captures a lazy expression. That is, there can't be some generic interface between caller and callee; the compiler must generate special-case code at every capture of an expression.
What you want makes lazy expressions into function calls.
Remember that the concept of inlining itself is not part of C++.
Tell that to `constexpr` functions. Those are required to be inlined (in the sense that a TU that uses a `constexpr` function must be able to see its definition). The same goes for template functions; you can't instantiate a template without having its definition visible to you.
On Wednesday, May 30, 2018 at 6:35:47 PM UTC+3, Nicol Bolas wrote:On Wednesday, May 30, 2018 at 10:46:39 AM UTC-4, floria...@gmail.com wrote:I agree that that would be a better semantic, but it implies a different ABI to that proposed (in passing) in P0927; rather than a single code pointer code whose invocation evaluates the expression, we need two code pointers with the second destructing temporaries, or a single code pointer with signature T(void* caller_stack, enum class action { evaluate, destruct_temporaries}). The caller would still need to reserve stack space for any temporaries in the lazily evaluated expression, but would not need to maintain a flag indicating whether temporaries had been constructed, as calling the code pointer to perform cleanup would be the responsibility of the callee.ABI is essentially irrelevant, because the only way you can have lazy expressions work correctly is if the function(s) that uses them are inlined, relative to the function that provided the expression.That's not true, there are many ways to implement this to have the correct behavior without inlining. Edward's solution is one.And as I said before, I don't what you call "the correct behavior" is what we want.The only cross-ABI way to make lazy expressions work is to make them an actual function, with the expected C++ semantics. At which point... they're not lazy expressions anymore. They're just a transparent way to turn an expression into a (lighter-weight) lambda.Yes, so what? C++ is ABI agnostic, you repeat this often enough. And now you want to specify language constructions to solve an implementation/ABI issue?No, I want to specify lazy expressions so that they're expressions, not function calls. So that they work exactly as if you had copy-and-pasted the expression at the site of use.Any particular reason to want them to behave exactly like that? Any pointer or examples of why this is required?To this point I understood layzies are like this (correct me if I am wrong)void f(){call(string("hello"));}becomesvoid f(){{auto s = string("hello");call(s);}}However the auto s = string("hello") is executed not before the function call, but from within the function call, going back to the original context with all the locals visible and so on.
That model seems to me pretty "natural" and easy to understand.
void foo([]->std::string str)
{
try { auto str2 = str(); }
catch(...)
{
}
//bottom of code
}
foo(std::string("foo") + func_that_throws() + std::string("bar"));
And what if the lazy evaluation doesn't throw, but the callee does:The caller will destroy the lazy scope as usual... (this is something I forgot in my implementation, but trivially fixable with a std::scope_exit for instance)Also, you seem to mix scope and storage: the scope of the lazy evaluation is completely and nicely defined: it is the same scope as if it were not lazy.
Can't we specify that given void foo([] -> T parm);, a call foo(expr) is treated as foo([&] -> T { return expr; }), and then let the semantics flow from that? (With a touch of magic so that foo isn't generic, but that should be fine for a parameterless lambda returning a specific type, right?)
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/CAHSYqdbpO97tt8JPT%3DJtk6zaJDzp07NbKWZTnFtGGD5j8vZp6A%40mail.gmail.com.
void print(std::string_view sv) { std::cout << sv << std::endl; }
void print_lazy([] -> std::string_view sv_expr) { std::cout << sv_expr << std::endl; }
template <class F> void print_fun(F&& f) { std::cout << std::forward<F>(f)() << std::endl; }
print(std::string("foo")); // This is correct
print_fun([] -> std::string_view { return std::string("foo"); } // temporary string will be destroyed before being printed
print_lazy(std::string("foo")); // What do we want?
@Nicol: Edward made a good and quick summary.
So in your example, when the throwing function throws, the lazy expression destroys the two strings already constructed (with classical exception handling for example), does not set the magic flag to true, and forward the exception to the callee (foo in your example).
So at the beginning of the catch, no temporaries are alive.
Then when the callee has finished, the caller will check the flag: it will still be false as it has not been set to true by the lazy expression, and will therefore not destroy the temporaries.
After the execution of the lazy expression (but in still in the callee), either all temporaries are alive (and the caller will need to destroy them), or none.
Concerning the scope, I agree with you that in the lazy case, the scope will start after, but it will still be the same (access to the same data, same behavior regarding exceptions) as the non-lazy expression scope.
The difference is: a lazy expression scope might/will outlive the scope that triggered its execution, but the rest is exactly the same.
int foo(bool t, []->int a1, []->int a2){ return t ? a1() : a2(); }
auto z1 = p ? bar() : baz();
auto z2 = foo(p, bar(), baz());
bool test([]->bool a1, []->bool a2, []->bool a3) { return a1() && a2() && a3(); }
auto z1 = foo() && bar() && baz();
auto z2 = test(foo(), bar(), baz());
T try_catch([]->T try_, []->T catch_)
{
try
{
return try_();
}
catch(...)
{
return catch_();
}
}
T try_again(int i, []->T try_)
{
while(i--)
{
try
{
return try_();
}
catch(...)
{
}
}
return T();
}
On Wednesday, May 30, 2018 at 2:21:57 PM UTC-4, floria...@gmail.com wrote:@Nicol: Edward made a good and quick summary.
So in your example, when the throwing function throws, the lazy expression destroys the two strings already constructed (with classical exception handling for example), does not set the magic flag to true, and forward the exception to the callee (foo in your example).
So at the beginning of the catch, no temporaries are alive.
Then when the callee has finished, the caller will check the flag: it will still be false as it has not been set to true by the lazy expression, and will therefore not destroy the temporaries.
After the execution of the lazy expression (but in still in the callee), either all temporaries are alive (and the caller will need to destroy them), or none.So, why is it OK for those temporaries to be destroyed if there's an exception, but they must be extended to outside of the evaluator if there is no exception? That's my point: if it's OK in one case for the temporaries to no longer exist, then it ought to be OK in general.
If the goal is to make lazy expression evaluation work like non-lazy evaluation, then this design fails. If an expression throws and it wasn't lazily captured, the caller is the first one who sees it (since they're the ones who evaluated it). If it was lazily captured, the function evaluating the expression can swallow it, such that the caller never sees it.
That is, the same logic you use to say that the code providing the expression ought to be providing storage for it is the same logic that could be used to say that the throw ought to originate in the caller, just like it would otherwise appear to. That is, if a lazy expression throws, the code between the evaluator and the provider of the expression unrolls immediately, and `catch` statements are only considered from the site of the one who originally provided the expression.
Concerning the scope, I agree with you that in the lazy case, the scope will start after, but it will still be the same (access to the same data, same behavior regarding exceptions) as the non-lazy expression scope.
The difference is: a lazy expression scope might/will outlive the scope that triggered its execution, but the rest is exactly the same.Everything is the same... except for the parts where it is different. But that means it's not the same. This would be the first time that the creation of an automatic object extends beyond the block that invokes that creation.You can't say that isn't new.
On Wednesday, May 30, 2018 at 7:39:38 PM UTC+2, Nicol Bolas wrote:
>
> On Wednesday, May 30, 2018 at 1:00:10 PM UTC-4, floria...@gmail.com wrote:
>
> > Le mercredi 30 mai 2018 18:47:14 UTC+2, floria...@gmail.com a écrit :
>
>
> I don't think this answered the question I asked, so allow me to provide some code:
>
> void foo([]->std::string str)
> {
> try { auto str2 = str(); }
> catch(...)
> {
> }
> //bottom of code
> }>> foo(std::string("foo") + func_that_throws() + std::string("bar"));>
> OK, the captured expression contains several temporaries.
>
> Let's say that the evaluation order for + for this implementation does things purely left-to-right. So it creates a temporary `string("foo")`. It then gets a temporary `string` from `func_that_throws()` and does `operator+` on them, resulting in a prvalue. That prvalue will then manifest a temporary to be added to the temporary `string("bar")`.
>
> Now of course, `func_that_throws` is so named because it throws. So my question is this: given the above evaluation order, which temporaries are still alive when you reach that `catch` statement?
>
> The only correct answer I can see is "none of them". And if that's the case, why is it OK for those temporaries to be destroyed if you reach "bottom of code" through the `catch` statement and not through non-exception execution?
>
> And if some of those temporaries are alive and some are not... how does that even work? Is there a boolean for each temporary in the captured expression to say if it's alive?
IMHO behavior of this two lines (with `z1` and `z2`) should be indistinguishable:or
int foo(bool t, []->int a1, []->int a2){ return t ? a1() : a2(); }
auto z1 = p ? bar() : baz();
auto z2 = foo(p, bar(), baz());
bool test([]->bool a1, []->bool a2, []->bool a3) { return a1() && a2() && a3(); }
auto z1 = foo() && bar() && baz();
auto z2 = test(foo(), bar(), baz());
Overall I think that parameter could be multiple times evaluated but it will return same value every time, storage for it will be from caller and caller will store flag that will check if paramere was aready evaluated.
std::array<int, 1000>* create_big([] -> std::array<int, 1000> a) {
return new std::array<int, 1000>(a()); //copy ellision: a() is a prvalue
}
auto *p = create_big({1, 2, ..., 1000});
std::array<int, 1000>* create_big([] -> std::array<int, 1000> a) {
std::array<int, 1000> b(a()); // copy ellision
return new std::array<int, 1000>(b); // no copy ellision here, b is not a prvalue
}
auto *p = create_big({1, 2, ..., 1000});
I think that for `[]->int a` we should have `&a() == &a()` this will be same temporary variable that is allocated on caller stack.
Only problem I see is order in with temporaries will be destroyed, probably it will be fixed one not depending on order in with params was call. Adding info about order will only increase implementation complexity and cost of each call of this parameter. This part I would leave UB.
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/6b7ba07f-33df-492c-a9fb-24d9c04662af%40isocpp.org.
bool andand([]->bool lhs, []->bool rhs)
{
Log("Performing andand on", lhs, " and ", rhs); // Variadic log with lazy parameters only evaluated if logging is enabled.
return lhs() && rhs(); // This is UB if logging is enabled!
}
// Variadic Log with lazy parameters only evaluated if logging is enabled.
template<typename T, typename... Ts> Log(T| head, Ts| tail)
{
if (log_enabled) {
cout << head;
Log(tail...);
}
}
bool andand(bool| lhs, bool| rhs)
{
Log("Performing andand on", lhs, " and ", rhs);
return lhs && rhs;
}
void foo(bool t, []->int a, []->int b, []->int c)
{
if (t)
{
a();
}
else
{
a(); //ok
}
if (t)
{
b();
}
if (!t) //compiler is dumb and do not try figure out that this path can't be taken
{
b(); //error!
}
do
{
c(); //error again, even if this can be call only once.
}
while(false);
}
Considering forwarding of lazy parameters to functions that may or may not use them it becomes very hard to use them if they can only be evaluated once, or the code must make copies just in case, which defeats the purpose of the feature.
Considering forwarding of lazy parameters to functions that may or may not use them it becomes very hard to use them if they can only be evaluated once, or the code must make copies just in case, which defeats the purpose of the feature. I don't think that avoiding a copy during initialization is a motivating purpose which should be allowed to rule the definition, instead we should focus on being able to mimic the shortcut operators and operator? and the classical logging scenario. Here is a nasty example:
bool andand([]->bool lhs, []->bool rhs)
{
Log("Performing andand on", lhs, " and ", rhs); // Variadic log with lazy parameters only evaluated if logging is enabled.
return lhs() && rhs(); // This is UB if logging is enabled!
}Apart from showing the problems with evaluate once principle this example rasies a couple of syntactical issues:1) How do we declare a variadic template with lazy parameters?2) Is is logical to forward-by-lazy by not using postfix(). If so, how many of us will add the () by mistake when calling Log, accidentally (but silently) evaluating the lazy parameter and forwarding the resulting value as a (very simple) lazy expression to Log?
3) Could it be considered to use postfix [] (empty bracket) to "derefer" a lazy parameter to make it stand out more from function application?Personally I still think that you should not have to annotate the use of a lazy parameter in the called function, just as the value/reference distinction this is something that the function author will have close to the top of her head when writing the code. This would nullify the 2) issue above. Maybe it could be complemented by a template function std::eager() which makes sure evaluation is done even if forwarding to a function that may take a lazy parameter. I don't have an example where this would be important though, it seems a bit backwards.
I think the best would be to view this as a new kind of reference, possibly using the | token to signify it. That is, it works like a reference, except that before the first use it is evaluated (using the flag to check this). As for other references there is no indicating at the usage site, as other (lvalue) references it is transparently forwarded to subordinate functions (for example Log in my example). The issue of how to write a template function has an obvious answer, and, I daresay, there should be no syntax issues as we have the example of using the & character which as an operator is parallel to | to indicate a type of reference already.
// Variadic Log with lazy parameters only evaluated if logging is enabled.
template<typename T, typename... Ts> Log(T| head, Ts| tail)
{
if (log_enabled) {
cout << head;
Log(tail...);
}
}
bool andand(bool| lhs, bool| rhs)
{
Log("Performing andand on", lhs, " and ", rhs);
return lhs && rhs;
}Looking at the big-array-copy scenario, I understand this as a case where a non-inlined function gets called and you want to redirect the constructor call of the lazy thunk (creating the by value "return" value) to do its construction directly in the receiving variable, presuming that, as the variable type is "big" the ABI will have the caller of the lazy thunk provide an address to construct the value in. The lazy thunk can use different constructors at different call sites which is nice in combination with not having to see the callee source code. This has significant performance advantage in the case that the value is large and does not have a lightweight move possibility. Apart from std::array all standard containers have cheap moves so the usefulness is further reduced compared to a regular by value parameter which is moved into the destination except for objects with large C or std arrays. Again: Is this functionality really so important that it warrants hijacking the original feature at the cost of making it less useful for its original purpose?
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/984d7434-303d-4808-b054-8b116f19e838%40isocpp.org.
Considering forwarding of lazy parameters to functions that may or may not use them it becomes very hard to use them if they can only be evaluated once, or the code must make copies just in case, which defeats the purpose of the feature. I don't think that avoiding a copy during initialization is a motivating purpose which should be allowed to rule the definition, instead we should focus on being able to mimic the shortcut operators and operator? and the classical logging scenario. Here is a nasty example:
bool andand([]->bool lhs, []->bool rhs)
{
Log("Performing andand on", lhs, " and ", rhs); // Variadic log with lazy parameters only evaluated if logging is enabled.
return lhs() && rhs(); // This is UB if logging is enabled!
}
bool andand([] -> bool lhs_lazy, [] -> bool rhs_lazy) {
std::optional<bool> lhs_v, rhs_v;
auto lhs = [&] () -> bool { if (!lhs_v) { lhs_v = lhs_lazy(); } return lhs_v.value(); };
auto rhs = [&] () -> bool { if (!rhs_v) { rhs_v = rhs_lazy(); } return rhs_v.value(); };
Log("Performing andand on", lhs(), " and ", rhs());
return lhs() && rhs();
}
Apart from showing the problems with evaluate once principle this example rasies a couple of syntactical issues:1) How do we declare a variadic template with lazy parameters?
template <class... Args>
void Log([] -> Args... args);
2) Is is logical to forward-by-lazy by not using postfix(). If so, how many of us will add the () by mistake when calling Log, accidentally (but silently) evaluating the lazy parameter and forwarding the resulting value as a (very simple) lazy expression to Log?
3) Could it be considered to use postfix [] (empty bracket) to "derefer" a lazy parameter to make it stand out more from function application?
Personally I still think that you should not have to annotate the use of a lazy parameter in the called function, just as the value/reference distinction this is something that the function author will have close to the top of her head when writing the code. This would nullify the 2) issue above. Maybe it could be complemented by a template function std::eager() which makes sure evaluation is done even if forwarding to a function that may take a lazy parameter. I don't have an example where this would be important though, it seems a bit backwards.
bool foo() {
bool lhs = bar(0); // this expression evaluation is forced here
return lhs && bar(1); // the evaluation of bar(1) might not happen
}
I think the best would be to view this as a new kind of reference, possibly using the | token to signify it. That is, it works like a reference, except that before the first use it is evaluated (using the flag to check this). As for other references there is no indicating at the usage site, as other (lvalue) references it is transparently forwarded to subordinate functions (for example Log in my example). The issue of how to write a template function has an obvious answer, and, I daresay, there should be no syntax issues as we have the example of using the & character which as an operator is parallel to | to indicate a type of reference already.
// Variadic Log with lazy parameters only evaluated if logging is enabled.
template<typename T, typename... Ts> Log(T| head, Ts| tail)
{
if (log_enabled) {
cout << head;
Log(tail...);
}
}
bool andand(bool| lhs, bool| rhs)
{
Log("Performing andand on", lhs, " and ", rhs);
return lhs && rhs;
}
Looking at the big-array-copy scenario, I understand this as a case where a non-inlined function gets called and you want to redirect the constructor call of the lazy thunk (creating the by value "return" value) to do its construction directly in the receiving variable, presuming that, as the variable type is "big" the ABI will have the caller of the lazy thunk provide an address to construct the value in. The lazy thunk can use different constructors at different call sites which is nice in combination with not having to see the callee source code. This has significant performance advantage in the case that the value is large and does not have a lightweight move possibility. Apart from std::array all standard containers have cheap moves so the usefulness is further reduced compared to a regular by value parameter which is moved into the destination except for objects with large C or std arrays. Again: Is this functionality really so important that it warrants hijacking the original feature at the cost of making it less useful for its original purpose?
I know other people already have answered your questions, but I would like to give my point of view.
Le samedi 2 juin 2018 00:52:34 UTC+2, Bengt Gustafsson a écrit :Considering forwarding of lazy parameters to functions that may or may not use them it becomes very hard to use them if they can only be evaluated once, or the code must make copies just in case, which defeats the purpose of the feature. I don't think that avoiding a copy during initialization is a motivating purpose which should be allowed to rule the definition, instead we should focus on being able to mimic the shortcut operators and operator? and the classical logging scenario. Here is a nasty example:
bool andand([]->bool lhs, []->bool rhs)
{
Log("Performing andand on", lhs, " and ", rhs); // Variadic log with lazy parameters only evaluated if logging is enabled.
return lhs() && rhs(); // This is UB if logging is enabled!
}If you really want something like that you could imagine doing the following:
bool andand([] -> bool lhs_lazy, [] -> bool rhs_lazy) {
std::optional<bool> lhs_v, rhs_v;
auto lhs = [&] () -> bool { if (!lhs_v) { lhs_v = lhs_lazy(); } return lhs_v.value(); };auto rhs = [&] () -> bool { if (!rhs_v) { rhs_v = rhs_lazy(); } return rhs_v.value(); };
Log("Performing andand on", lhs(), " and ", rhs());
return lhs() && rhs();
}This would allow the kind of behavior you want (parameters will be evaluated only if they are needed), while keeping lazy arguments returning pr-values.We would need to standardize lambda captures relating to lazy parameters, but it would be worth it.However, if you allow multiple evaluation of the expression: either you will not be able to return a pr-value (with caching), or you will have problems with overlapping lifetime of the same objects...So you can implement what you want with what I propose, but I cannot implement what I want with what you propose.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/cc20625c-804f-4ad2-8c38-ce443e7314ec%40isocpp.org.
Hello
I am more than a little concerned about all this talk of multiple evaluation
of lazy arguments.
I presume that everone agrees that given
void f([]->int);
then after
int a = 10;
f(a++);
the value of a is either 10 or 11, no other values are possible.
(BTW, What a great obfuscation tool this would be even with this limitation)
This in turn means that any second evaluation of the lazy expression must
be forbidden unless the compiler can prove it to be side effect free.
It should be possible to implement multiple evaluation without caching by storing lifetime extended temporaries in a conventionally located stack frame and then not restoring (or only partially restoring) the stack pointer on return from evaluation. This should be technically possible on any platform where alloca is implementable.The stack would look like: (growing downwards)callercalleelazy1a temporarieslazy2 temporarieslazy1b temporariesinner functionHere lazy1 is evaluated twice within the same expression, giving two sets of temporaries. The temporaries would be destructed and their stack space reclaimed at the end of the full-expression in the callee. Note that the stack space used is bounded by the size of the expression in the callee.This would have the advantage that scope, lifetime and stack location would be more conventionally correlated, and may be technically easier to implement than storing temporaries within the stack frame of the caller.
Le dimanche 3 juin 2018 13:16:18 UTC+2, Magnus Fromreide a écrit :Hello
I am more than a little concerned about all this talk of multiple evaluation
of lazy arguments.
I presume that everone agrees that given
void f([]->int);
then after
int a = 10;
f(a++);
the value of a is either 10 or 11, no other values are possible.
(BTW, What a great obfuscation tool this would be even with this limitation)This example is really great to show mutliple evaluations should not be allowed, period.BTW, the example I showed with manually caching the result in a lambda gives this behavior: the lazy expression is evaluated once.
Unless the compiler inline both the caller and the lazy expression, it will not be able to prove this side-effect free property.
This in turn means that any second evaluation of the lazy expression must
be forbidden unless the compiler can prove it to be side effect free.
And if both are inlined, all technical issues disappear.@EdwardIt should be possible to implement multiple evaluation without caching by storing lifetime extended temporaries in a conventionally located stack frame and then not restoring (or only partially restoring) the stack pointer on return from evaluation. This should be technically possible on any platform where alloca is implementable.The stack would look like: (growing downwards)callercalleelazy1a temporarieslazy2 temporarieslazy1b temporariesinner functionHere lazy1 is evaluated twice within the same expression, giving two sets of temporaries. The temporaries would be destructed and their stack space reclaimed at the end of the full-expression in the callee. Note that the stack space used is bounded by the size of the expression in the callee.This would have the advantage that scope, lifetime and stack location would be more conventionally correlated, and may be technically easier to implement than storing temporaries within the stack frame of the caller.Unfortunatley, if the lazy expression "calls" alloca, the stack memory will be deallocated when the lazy expression ends its scope. That's why allocating this memory in the caller stack frame is much easier.
Also, how would you track down all destructors that would be needed to be called?
If you want multiple scopes, you will need heap memory allocation at some point, which is really not needed to have a very useful feature.
You can always create more complex objects from the simple lazy expression that cannot be evaluated multiple times to achieve your goal.
Also, what if the lazy expression calls new: how do you track down the multiple allocations?
If the lazy expression is very expensive, do you really want do execute the all thing again?
Caching is the only sensible way to allow multiple evaluations, but this forbids pr-values, with all the issues mentionned before.And as I showed earlier, if a caller needs to cache the result, it can do it. Whereas other proposals do not/cannot solve altogether the pr-value issue, the lifetime issues and the performance issue.Of course it needs a bit of boilerplate code to actually do the caching, but this use case should be rare enough. And the caching code is really understandable and easy to come up with, especially if you know the lazy expression will always be evaluated.One solution might be to have both: [] -> lazy parameter without caching and [=] -> lazy parameter with caching (with no consideration on how it is implemented).Anyway, if you need multiple evaluations, why do you pass a lazy expression and not a callable? That would make your intent much clearer.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/5cdf347c-a54a-49bb-905f-dc392fedf121%40isocpp.org.
In general, I agree with all of the above. I am concerned that there may be use cases (e.g. current macros) that would benefit from multiple evaluation (non-cached) being defined, and I would not want multiple evaluation to be excluded that I believe I have shown can be avoided.
On Sun, 3 Jun 2018, 13:21 , <floria...@gmail.com> wrote:
Le dimanche 3 juin 2018 13:16:18 UTC+2, Magnus Fromreide a écrit :Hello
I am more than a little concerned about all this talk of multiple evaluation
of lazy arguments.
I presume that everone agrees that given
void f([]->int);
then after
int a = 10;
f(a++);
the value of a is either 10 or 11, no other values are possible.
(BTW, What a great obfuscation tool this would be even with this limitation)This example is really great to show mutliple evaluations should not be allowed, period.BTW, the example I showed with manually caching the result in a lambda gives this behavior: the lazy expression is evaluated once.That depends, though. We're perfectly happy to write `while (a++) ...;` and it's not unreasonable to think that this facility might be used for functions with control flow that requires multiple invocation.
Unless the compiler inline both the caller and the lazy expression, it will not be able to prove this side-effect free property.
This in turn means that any second evaluation of the lazy expression must
be forbidden unless the compiler can prove it to be side effect free.
And if both are inlined, all technical issues disappear.@EdwardIt should be possible to implement multiple evaluation without caching by storing lifetime extended temporaries in a conventionally located stack frame and then not restoring (or only partially restoring) the stack pointer on return from evaluation. This should be technically possible on any platform where alloca is implementable.The stack would look like: (growing downwards)callercalleelazy1a temporarieslazy2 temporarieslazy1b temporariesinner functionHere lazy1 is evaluated twice within the same expression, giving two sets of temporaries. The temporaries would be destructed and their stack space reclaimed at the end of the full-expression in the callee. Note that the stack space used is bounded by the size of the expression in the callee.This would have the advantage that scope, lifetime and stack location would be more conventionally correlated, and may be technically easier to implement than storing temporaries within the stack frame of the caller.Unfortunatley, if the lazy expression "calls" alloca, the stack memory will be deallocated when the lazy expression ends its scope. That's why allocating this memory in the caller stack frame is much easier.Sorry, I didn't mean to imply that the lazy expression would call alloca. Rather it would behave like alloca, giving the stack pointer a different value on exit than on entry.
Also, how would you track down all destructors that would be needed to be called?That would be the job of the lazy expression routine; the callee would invoke the lazy expression routines in destruct mode as many times as they were invoked in evaluate mode and in reverse.Recall that you need a destruct mode to be invoked by the callee to ensure that destructors of temporaries are called at the end of the correct statement in the callee.
void print(std::string_view sv) { std::cout << sv << std::endl; }
std::string_view lazy([] -> std::string s) { return s; }
std::string_view eager(const std::string & s) { return s; }
print(eager("hello world!"));
print(lazy("hello world!"));
If you want multiple scopes, you will need heap memory allocation at some point, which is really not needed to have a very useful feature.I don't see why?
You can always create more complex objects from the simple lazy expression that cannot be evaluated multiple times to achieve your goal.Yes, but then you lose prvalue-ness, as you have pointed out.
Also, what if the lazy expression calls new: how do you track down the multiple allocations?Assuming the new-expressions are adopted by a smart pointer, normal RAII would apply.
int i = /* ... */;
int* p = i > 0 ? new int(i) : nullptr;
/* ... */
if (p) delete p;
If the lazy expression is very expensive, do you really want do execute the all thing again?Probably not, but that should generally be the user's choice.
Caching is the only sensible way to allow multiple evaluations, but this forbids pr-values, with all the issues mentionned before.And as I showed earlier, if a caller needs to cache the result, it can do it. Whereas other proposals do not/cannot solve altogether the pr-value issue, the lifetime issues and the performance issue.Of course it needs a bit of boilerplate code to actually do the caching, but this use case should be rare enough. And the caching code is really understandable and easy to come up with, especially if you know the lazy expression will always be evaluated.One solution might be to have both: [] -> lazy parameter without caching and [=] -> lazy parameter with caching (with no consideration on how it is implemented).Anyway, if you need multiple evaluations, why do you pass a lazy expression and not a callable? That would make your intent much clearer.In general, I agree with all of the above. I am concerned that there may be use cases (e.g. current macros) that would benefit from multiple evaluation (non-cached) being defined, and I would not want multiple evaluation to be excluded that I believe I have shown can be avoided.
Sorry, missed a few words there: I would not want multiple evaluation to be excluded *on the basis of technical considerations* that I believe I have shown can be avoided.
Le lundi 4 juin 2018 07:31:03 UTC+2, Edward Catmur a écrit :On Sun, 3 Jun 2018, 13:21 , <floria...@gmail.com> wrote:
Le dimanche 3 juin 2018 13:16:18 UTC+2, Magnus Fromreide a écrit :Hello
I am more than a little concerned about all this talk of multiple evaluation
of lazy arguments.
I presume that everone agrees that given
void f([]->int);
then after
int a = 10;
f(a++);
the value of a is either 10 or 11, no other values are possible.
(BTW, What a great obfuscation tool this would be even with this limitation)This example is really great to show mutliple evaluations should not be allowed, period.BTW, the example I showed with manually caching the result in a lambda gives this behavior: the lazy expression is evaluated once.That depends, though. We're perfectly happy to write `while (a++) ...;` and it's not unreasonable to think that this facility might be used for functions with control flow that requires multiple invocation.For that kind of stuff, I would prefer a "macro-like" approach. I know some people were working on that. It's personal taste here.
Unless the compiler inline both the caller and the lazy expression, it will not be able to prove this side-effect free property.
This in turn means that any second evaluation of the lazy expression must
be forbidden unless the compiler can prove it to be side effect free.
And if both are inlined, all technical issues disappear.@EdwardIt should be possible to implement multiple evaluation without caching by storing lifetime extended temporaries in a conventionally located stack frame and then not restoring (or only partially restoring) the stack pointer on return from evaluation. This should be technically possible on any platform where alloca is implementable.The stack would look like: (growing downwards)callercalleelazy1a temporarieslazy2 temporarieslazy1b temporariesinner functionHere lazy1 is evaluated twice within the same expression, giving two sets of temporaries. The temporaries would be destructed and their stack space reclaimed at the end of the full-expression in the callee. Note that the stack space used is bounded by the size of the expression in the callee.This would have the advantage that scope, lifetime and stack location would be more conventionally correlated, and may be technically easier to implement than storing temporaries within the stack frame of the caller.Unfortunatley, if the lazy expression "calls" alloca, the stack memory will be deallocated when the lazy expression ends its scope. That's why allocating this memory in the caller stack frame is much easier.Sorry, I didn't mean to imply that the lazy expression would call alloca. Rather it would behave like alloca, giving the stack pointer a different value on exit than on entry.(alloca is not a function anyway, it is just some arithmetic on pointers)The problem is: if you give a different stack pointer to the callee, it would need to adjust back the pointer itself to access its data. And what if the callee needs to allocate some stack after evaluating a lazy expression? Where would the new stack frame go? How does the caller know where it would go?After some thinking, I agree it might be possible to implement, but at what costs?
Also, how would you track down all destructors that would be needed to be called?That would be the job of the lazy expression routine; the callee would invoke the lazy expression routines in destruct mode as many times as they were invoked in evaluate mode and in reverse.Recall that you need a destruct mode to be invoked by the callee to ensure that destructors of temporaries are called at the end of the correct statement in the callee.I don't recall that. It was suggested, but I think the lazy expression temporaries must outlive the caller expression (not only the callee one).The eager expression works perfectly fine now: the std::string temporary lifetime ends at the semicolon (https://godbolt.org/g/6xV4aM).
void print(std::string_view sv) { std::cout << sv << std::endl; }
std::string_view lazy([] -> std::string s) { return s; }
std::string_view eager(const std::string & s) { return s; }
print(eager("hello world!"));
print(lazy("hello world!"));I would expect the second to also works for the very same reason.This can happen only if the caller is the one to destroy the lazy expression temporaries.
If you want multiple scopes, you will need heap memory allocation at some point, which is really not needed to have a very useful feature.I don't see why?Maybe not, but very hard. Especially to make the destruction happens in the caller as I explained just above.You can always create more complex objects from the simple lazy expression that cannot be evaluated multiple times to achieve your goal.Yes, but then you lose prvalue-ness, as you have pointed out.I don't see any valid use case where these 2 features might be needed at the same time. If you really want multiple evaluations also allowing prvalues, why callables are not enough?
Also, what if the lazy expression calls new: how do you track down the multiple allocations?Assuming the new-expressions are adopted by a smart pointer, normal RAII would apply.You don't need RAII for this code to be correct:Why would it be different with custom functions, or custom types?
int i = /* ... */;
int* p = i > 0 ? new int(i) : nullptr;
/* ... */
if (p) delete p;I agree smart pointers are much safer and should be used everywhere a new/delete was used. But this is still valid code.
If the lazy expression is very expensive, do you really want do execute the all thing again?Probably not, but that should generally be the user's choice.A user might not be able to tell if an expression is expensive or not. It will be even more true with this because it will be simpler to call complex constructors without even noticing (and that's part of the point).Caching is the only sensible way to allow multiple evaluations, but this forbids pr-values, with all the issues mentionned before.And as I showed earlier, if a caller needs to cache the result, it can do it. Whereas other proposals do not/cannot solve altogether the pr-value issue, the lifetime issues and the performance issue.Of course it needs a bit of boilerplate code to actually do the caching, but this use case should be rare enough. And the caching code is really understandable and easy to come up with, especially if you know the lazy expression will always be evaluated.One solution might be to have both: [] -> lazy parameter without caching and [=] -> lazy parameter with caching (with no consideration on how it is implemented).Anyway, if you need multiple evaluations, why do you pass a lazy expression and not a callable? That would make your intent much clearer.In general, I agree with all of the above. I am concerned that there may be use cases (e.g. current macros) that would benefit from multiple evaluation (non-cached) being defined, and I would not want multiple evaluation to be excluded that I believe I have shown can be avoided.For now, I don't see any use case that would really need this syntax boilerplate free at call site feature.I really believe that this kind of problems should be solved either with callables or "macro"-like constructions.Also multiple evaluation could also be added afterwards if there is a need for it. As long as we say multiple evaluation is undefined, then extending it would be possible.
Sorry, missed a few words there: I would not want multiple evaluation to be excluded *on the basis of technical considerations* that I believe I have shown can be avoided.More than technical considerations, it is performance considerations. You want the fast cases being fast: if the lazy expression is just a literal, you really don't want all the implementation boilerplate to deal with multiple evaluations and temporaries bookkeeping.Your solution makes this use case inefficient because the callee won't know the lazy expression is as simple as that.In the implementation I proposed, this is as fast as it could get without inlining: just a call to a function returning the literal.
--
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/5oUZysJB4HE/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@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/ff30ffc4-f898-4b10-a26e-a6e9c2560642%40isocpp.org.
Hello everyone,
I would want to propose a new way to pass parameters to functions.
Currently, we have 2 way to pass parameters: by value, and by reference.
(Passing by pointer is really passing by value the address)
However, none of those allow delayed evaluation of parameters.
But this would be useful in some cases: for example overloading operator&& and operator|| (and also operator?: when it will be overloadable).
It could also be used to implement a select function defined like this:
float select(int i, float a, float b, float c) {
switch (i) {
case 0:
return a;
case 1:
return b;
case 2:
return c;
}
}
The delayed evaluation is important to allow a parameter with side effects not to be executed if not needed, avoiding undefined behaviors:
float *p;
if (p && *p > 0) { ... } // this is valid
bool AND(bool a, bool b) {
return a && b;
}
if (AND(p, *p > 0)) { ... } // This is not
I thought of 4 ways to express that:
Type qualifier + implicit evaluation:
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return a;
case 1:
return b;
case 2:
return c;
}
}
Type qualifier + operator:
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return @a;
case 1:
return @b;
case 2:
return @c;
}
}
Type qualifier + callable:
float select(int i, float @ a, float @ b, float @ c) {
switch (i) {
case 0:
return a();
case 1:
return b();
case 2:
return c();
}
}
STL type:
float select(int i,std::expression<float>
a,b,
std::expression<float>
c) {
std::expression<float>
switch (i) {
case 0:
return a();
case 1:
return b();
case 2:
return c();
}
}
In all the 3 cases, when the select function is called, a functor-like object is created computing the expression, and this object is then passed to the function.
float* p;
int i;
float r = select(i, 1.f, *p, std::sqrt(*p));
// equivalent to:
float r;
switch (i) {
case 0:
r = 1.f;
break;
case 1:
r = *p;
break;
case 2:
r = std::sqrt(*p);
break;}
I really have the impression this could be very interesting in some corner cases where there is currently no alternatives to do that safely (and efficiently).
Would this be sensible? Were there any attempt to formalize passing by expression?
Which way of expressing this should be preferred?
I would be glad to answer any question.
Florian
struct Foo {
int i;
Foo(const Foo&) = delete;
};
template <class T>
struct Bar {
T* p = nullptr;
~Bar() { delete p; }
void set(T const& v) { p = new T(v); }
void set_lazy([] -> T v) { p = new T(v()); }
void set_func(std::function_ref<T()> f) { return new T(f()); }
template <class... Args>
void emplace(Args&&... args) { p = new T(std::forward<Args>(args)...); }
};
Bar<Foo> bar;
// We want to construct Foo from { .i = 1 } // designated initializer list
bar.set({ .i = 1 }); // Foo is not copyable
bar.set_lazy({ .i = 1 }); // this is fine
bar.emplace(/* what here? */); // designated initializer list construction is not a constructor
bar.set_func([]() -> Foo { return { .i = 1 }; }); // name has to be mentionned here
I have been pondering this proposal and P0927 for a while to see if I could reconcile the two use cases of P0927 into one decent feature. I failed, and I think that they must be served by different features.The first use case is to avoid evaluating argument expressions if not needed, which may be a performance optimization such as in a logging case or semantically important as in a shortcut operator. The parameter of std::optional::value_or would also be a good use case if it wasn't too late for that. Here an important property is that the call site does not have to care whether the parameter is lazy and that the argument can be forwarded to another lazy parameter without problems. These two requirements mean that temporaries of the argument expression must be preserved until the callee returns and that the parameter must be allowed to be evaluatated more than once (or forwarding would be ridiculously useless).
I don't clearly get your point.
If in your second use case, you don't want multiple evaluations, then both use cases are possible within a single feature (as I already showed earlier).
If you do, std::function_ref would be the answer.
There is a case that would be improved with
struct Foo {
int i;
Foo(const Foo&) = delete;
};
template <class T>
struct Bar {
T* p = nullptr;
~Bar() { delete p; }
void set(T const& v) { p = new T(v); }
void set_lazy([] -> T v) { p = new T(v()); }
void set_func(std::function_ref<T()> f) { return new T(f()); }};
template <class... Args>
void emplace(Args&&... args) { p = new T(std::forward<Args>(args)...); }
Bar<Foo> bar;
// We want to construct Foo from { .i = 1 } // designated initializer list
bar.set({ .i = 1 }); // Foo is not copyable
bar.set_lazy({ .i = 1 }); // this is fine
bar.emplace(/* what here? */); // designated initializer list construction is not a constructor
bar.set_func([]() -> Foo { return { .i = 1 }; }); // name has to be mentionned here
In this case, set cannot work because Foo is non-copyable.
emplace cannot work because it is not a constructor (and it is not type-erased).
And you would need to name the type to make set_func works (even with an hypothetical terse lamb
da syntax). While set_lazy would have all the good properties we want here.
I understand that there was hard to see my conclusion here, but I showed an example earlier in this thread where a shortcut operation, as part of its implementation, logs the incoming parameter value by forwarding it to a logging function that may, or may not, actually evaluate the parameter. Thus when log returns, the main callee does not know if the parameter has already been evaluated as it depend on whether logging is enabled. Thus if it requires the parameter value it must be able to do so even if log() also did.bool andand([]->bool lhs, []->bool rhs)
{
Log("Performing andand on", lhs, " and ", rhs); // Variadic log with lazy parameters only evaluated if logging is enabled.
return lhs() && rhs(); // This is UB if logging is enabled!
}
bool andand([] -> bool lhs_lazy, [] -> bool rhs_lazy) {
std::optional<bool> lhs_v, rhs_v;
auto lhs = [&] () -> bool { if (!lhs_v) { lhs_v = lhs_lazy(); } return lhs_v.value(); };
auto rhs = [&] () -> bool { if (!rhs_v) { rhs_v = rhs_lazy(); } return rhs_v.value(); };
Log("Performing andand on", lhs(), " and ", rhs());
return lhs() && rhs();
}
template <class T>
class multiple_execution {
private:
[] -> T lazy;
std::optional<T> val;
void execute() {
if (!val) {
val = lazy();
}
}
public:
multiple_execution([] -> T lazy) : lazy(lazy) {}
multiple_execution() = delete;
multiple_execution(const multiple execution&) = delete;
multiple_execution& operator=(const multiple_execution&) = delete;
~multiple_execution() = default;
T& operator()() & {
execute();
return val.value();
}
T&& operator()() && {
execute();
return *std::move(val)
}
};
bool andand([] -> bool lhs_lazy, [] -> bool rhs_lazy) {
multiple_execution<bool> lhs(lhs_lazy()), rhs(rhs_lazy());
Log("performing andand on ", lhs(), " and ", rhs());
return lhs() && rhs();
}
Den torsdag 14 juni 2018 kl. 13:37:16 UTC+2 skrev floria...@gmail.com:I don't clearly get your point.
If in your second use case, you don't want multiple evaluations, then both use cases are possible within a single feature (as I already showed earlier).The problem is that if the first use case is for a by value parameter and evaluates it multiple times the type must be copyable. So we can't combine allowing multiple usage with allowing non-copyability.If you consider the code that the compiler has to generate for the lazy parameter thunk I can't see that we can get both in the same feature. The only possibility would be for the compiler to generate different code depending on whether theparameter type is copyable. This could be possible but I don't have time right now to think that trhough.
struct Point {
float x, y, z;
};
Bar<std::atomic<Point>> bar;
bar.set_lazy({{ .x = 1.f, .y = 1.f, .z = 1.f }});
On Fri, Oct 12, 2018 at 10:29 AM <floria...@gmail.com> wrote:
>
>
> Le vendredi 12 octobre 2018 02:28:40 UTC+2, Miguel Ojeda a écrit :
>>
> Jason's proposal is interesting, and I also thought it could be used to implement lazy parameters, but actually, some stuff are just not possible with it.
> The main thing I see is: the code must be visible where it is used.
> With lazy parameters, you don't need to have the callee code visible from the caller. It can be compiled within another Translation Unit.
> This can seriously decrease binary size (the same way type erasure does).
Indeed, that is what I meant when I said:
>> The limitation, of
>> course, is that this is a compile-time thing only (no real functions
>> -- but if you need that, you can still pass normal lambdas).
In my view, macro-like expansion is something we are 100% missing from
the language (i.e. cannot be done except with macros); while lazy
parameters are something that you can approximate quite nicely today
(e.g. lambdas). Therefore, I value much more the former!
>
> Also, what about lifetime of temporaries?
> As far as I understand, the lifetime of the temporary will be the same as with a macro/code copy-pasted.
> And this is something that is simply just not true with lazy parameters.
> The lifetime of the lazy parameters is extended as-if it were normal parameters (until the end of the caller expression).
> So no surprise for the user.
> (If you want more details, you can read the thread again, there are many examples)
> And if you want to integrate these lifetime rules within Jason's proposal, then it would become much more complex (maybe more complex than lazy parameters?).
Yep, I did, and I am with Nicol here -- there is no point in
integrating "lifetime rules" for the macro-like approach, that is the
beauty of it.
>
> A last point about multiple evaluation, this is tricky for the user on its own, and it's not needed most of the time.
It is tricky in the same ways as macros, so basically already known by
C programmers; i.e. it is an advantage w.r.t. introducing lazy
arguments (which requires a quite more complex proposal, as you said).
> So that's fine for "macro-like" approach, but for lazy parameters, I don't think that's a good idea.
Agreed: I didn't say it was appropriate for the lazy-arguments approach.
> Currently in c++ (macros are not C++), there is no expression that could lead to multiple evaluations of sub-expression. But there already are some expression where sub-expressions can be discarded without being evaluated.
Macros are C++, what do you mean? We try to avoid them because they
are on their own phase of translation which gives them shortcomings --
but that is one of the points of the proposal(s): as we have done
previously many times in C++, we are trying to reduce reliance on
macros).
Now, I agree that it is worth discussing whether using this vs. a
macro would be beneficial or not (i.e. whether the proposal is worth
its disadvantages).
>
> Jason's proposal is interesting on its own and has many use-cases, but I think lazy parameters is not one of them.
> That's why I think the two should be pursued independently.
Isn't the only non-overlapping use case the ability to use declare
functions which use lazy arguments without a definition? (If I am
missing other significant use cases that cannot be provided by a
macro-like proposal, please let me know!).
--
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-proposal...@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/CAFk2RUaJDPqHUiPSoJn_gnDJoVxOn1SNGHDrhhUEksDVp40OCg%40mail.gmail.com.
I do not see the value of a language feature for lazy evaluation.Any solution would ultimately look a lot like a lambda, maybe with a shorter syntax but it would essentially look and behave the same, so there would be a little gain.A more generic solution for that particular use case would be to simplify lambda everywhere (see for example http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0573r2.html )
However, I do think that superseding all or part of the pre-processor macros (token soup injection), by a language level feature is something that would be incredibly powerful,And as such, Jason proposal is a much better solution overall.
But I think it's important to think about what we want.
- That language feature results in visible AST-nodes
- Scoped names - that seems evident but it's one of the issues with C-macro
- The definition should be checked
- The parameters should be checked - All expressions have the nice property of having a type, so parametric expression should be able to constrain on type/concepts
I think it's important to keep in mind that what Jason proposes and similar proposals never call anything or return anything, regarding of whether we end up on a solution building on top of code injection/expression reflection or not, the result is basically the same: shifting ast-nodes around so name lookup etc is a non-issue - but you can never escape the scope.
(Having the injectee/parametric expression being able to see in each other scope would probably be of little value and a pretty big nightmare)A code-injection based system can be extended to be able to inject statements in the current scope.So it would be possible to have sanitary language-level macros that have expressionsas parameters and expand to one or several statements.Herb and Andrew also demonstrated ways to construct identifiers and typename at compile time and all of these pieces can fit nicely together.However, that does not, unfortunately, replaces all macro use cases.Namely, I have no idea how to inject partial statements, which is something a lot of test frameworks do, for exampleThere is no one unique replacement for macros but instead a multitude of solutions.And I don't think it would be a bad thing to have a gradient of solutions (function, constexpr functions, constexpr parameters, parametric expressions, something else in the future) as long as the whole is cohesive.
--
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-proposal...@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/58d6ae1e-3734-452f-b490-231b1a921c10%40isocpp.org.
I'm not sure no marking at the call site would be a good idea.You most likely want some syntax notifying "This thing that you think is evaluating may in fact, not be evaluated"There is a value in being explicit, and not giving the same syntax to multiple constructsfunction_call( [x, y] => x + y);
Similarly, I think we probably want some syntax for jason's proposal to distinguish "this is evaluating the parameters and invoking a function" from "this expands to some more code"Rust has the `!` syntax for this reason (printn!("I am a macro"))
On 12/10/2018 07.52, Corentin wrote:
> However, that does not, unfortunately, replaces all macro use cases.
> Namely, I have no idea how to inject partial statements, which is something
> a lot of test frameworks do, for example
Indeed, and I was just thinking about this very same issue.
Other things for which we don't currently have a replacement:
#define call(x, ...) log(#x, x, __VA_ARGS__) // ?
#define signal protected
#define with(x) if(x; true)
I'm not *entirely* certain if it's possible with reflection to implement
the first as a non-macro. Even if *that* case is possible, I'd have to
question if all possible uses of stringifying can be replaced,
especially those that stringify entire expressions (again, something
often found in test frameworks).
For the second, I doubt any replacement is forthcoming.
For the third, in particular, I'm thinking of macros that expand to a
statement which opens a scope, i.e. `if`, `while`, `for`, ... where the
macro is followed by a statement or an open brace which is associated
with the macro. I suppose we could define a way to make "new control
statements" that would always have this property (in fact, that might
even be a very interesting feature, especially if it incorporates
mechanisms to execute code at the end of the scope and possibly to
decide if the scope should be repeated like a loop). However AFAIK the
current "parametric expressions" proposal would not cover these cases.
--
Matthew
--
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-proposal...@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/c4905596-60a8-cbc1-1e03-2cb811a3543c%40gmail.com.
On Fri, Oct 12, 2018 at 12:43 PM <floria...@gmail.com> wrote:
>
> Le vendredi 12 octobre 2018 12:03:57 UTC+2, Miguel Ojeda a écrit :
>>
>> On Fri, Oct 12, 2018 at 10:29 AM <floria...@gmail.com> wrote:
>>
>> It is tricky in the same ways as macros, so basically already known by
>> C programmers; i.e. it is an advantage w.r.t. introducing lazy
>> arguments (which requires a quite more complex proposal, as you said).
>
>
> Even the most experienced C programmer can be tricked by multiple evaluations within macros: either because they didn't realize it was a macro, or more subtle because experienced programmers write macros in such a way that parameters are not evaluated more than once (for safety reasons).
>
Anyone can make mistakes with any feature, that is not the really
point... Macros are already understood by all C++ (and C) programmers.
They are not *that* tricky, compared to other C++ features.
>> Macros are C++, what do you mean? We try to avoid them because they
>> are on their own phase of translation which gives them shortcomings --
>> but that is one of the points of the proposal(s): as we have done
>> previously many times in C++, we are trying to reduce reliance on
>> macros).
>
>
> (No, the preprocessor is not part of C++. C++ acknowledges the existence of the preprocessor, but not really more. But that's just being picky...).
No. Please refer to [cpp] or [lex] in the standard.
>> Isn't the only non-overlapping use case the ability to use declare
>> functions which use lazy arguments without a definition? (If I am
>> missing other significant use cases that cannot be provided by a
>> macro-like proposal, please let me know!).
>
>
> You miss the lifetime rules. Those are crucial.
I don't see what the lifetime rules buys you that the macro-like
approach doesn't. Please provide an example.
void print(std::string_view sv) { std::cout << sv << std::endl; }
std::string_view eager(const std::string & s) {
std::string_view sv{s};
return sv;
}
std::string_view lazy([] -> std::string s) {
std::string_view sv{s};
return sv;
}
std::string_view call(std::function<std::string()> f) {
std::string_view sv{f()};
return sv;
}
using macro(auto s) {
std::string_view sv{s};
return sv;
}
print(eager(std::string("hello world!"))); // here is fine
print(call([]{ return std::string("hello world!");})); // here, `std::string("hello world")` will be destroyed before calling `print`
print(lazy(std::string("hello world!"))); // here, we want it to have the same lifetime as for `eager`
print(macro(std::string("hello world!"))); // what here ?