On Monday, 29 April 2024 at 19:17:47 UTC, Paul Backus wrote:
> On Thursday, 25 April 2024 at 17:56:59 UTC, Quirin Schroll wrote:
> https://github.com/Bolpat/DIPs/blob/2bfef0b05e99c5448b416078da2e743482e3193d/DIPs/1NNN-QFS.md
Yet another non-orthogonal language feature that all generic code will have to go out of its way to account for until the end of time. ref
is already bad enough; the last thing we need is another feature that repeats the exact same design mistakes.
TL;DR: While auto enum
is similar to auto ref
in that it infers a storage class based on the argument passed, forwarding is a non-issue for auto enum
and even auto enum auto ref
. Analysis is below.
> For example: currently, in order to forward an argument correctly in generic code, we must write the following:
void forwardTo(alias fun, T)(auto ref T arg)
{
import core.lifetime: forward;
fun(forward!arg);
}
Notice how much overhead is required just to deal with the ref
storage class.
I agree that forwarding in its current form is indeed a nuisance.
> If this proposal were accepted, we would have to replace all instances of the above with the following:
void forwardTo(alias fun, T)(auto enum auto ref T arg)
{
import core.lifetime: forward;
static if (__traits(isEnum, arg))
fun(arg);
else
fun(forward!arg);
}
Needless to say, the vast majority of D programmers are not going to bother with this, which means that we will forever be plagued by buggy handling of enum
parameters in our library code (just as we are already plagued by buggy handling of ref
parameters).
I do not think it is a good idea to introduce language features that set future D programmers up for failure like this.
You’re right in that forward
better be mentioned in the DIP. Writing an answer to this post, I found out that not only could forward
easily be adapted to recognize enum
parameters and handle them properly by leaving them alone, forward
actually would handle them correctly today.
First things first, contrary to auto ref
, an auto enum
parameter bound to a compile-time constant (i.e. one that becomes enum
) would stay a compile-time constant (and a run-time value would stay a run-time value), so forwarding auto enum
is automatic (it requires nothing in terms of written code to happen). That is contrary to auto ref
where no matter how the argument was passed (i.e. no matter whether the parameter becomes ref
), the parameter itself is always an lvalue and binds to/prefers ref
when passed to another function. This is why forwarding auto ref
must be explicit.
Another aspect is that there is no enum ref
parameters, so auto enum auto ref
is non-orthogonal by design: It can become enum
(pass as compile-time constant), ref
(pass by reference) or none (pass by value).
Assuming the following code (which apart from additional auto enum
is identical to the example of currently existing code):
void forwardTo(alias fun, T)(auto enum auto ref T arg)
{
import core.lifetime: forward;
fun(forward!arg);
}
How does forward
handle the three cases? The cases ref
and pass-by-value are clear, they are as they are today. As far as the function template implementation is concerned, the enum
case is equivalent to:
void forwardTo(alias fun, T, T arg)()
{
import core.lifetime: forward;
fun(forward!arg);
}
That is valid code today!
void main()
{
immutable myInt = 10;
forwardTo!(
(auto ref x) { pragma(msg, __traits(isRef, x)); },
int,
myInt
);
}
It prints false
. This means, forward
passed the compile-time constant to the lambda unchanged and a compile-time constant is seen as an rvalue. Note that myInt
could absolutely be bound as an lvalue.
The reason for this is that forward
not only recognizes some storage classes (ref
among them), it also checks if move
would work and if it doesn’t, it does not use it. That is because template value parameters are rvalues and enum
parameters would be rvalues as well, and move
only works on lvalues.