Passing rvalues with a reference is unintuitive with `preview=rvaluerefparam`
D's ternary operator (? :) can have reference semantics despite being rvalues. For example, (a() ? a() : b()).mutate() mutates a() if a() returns by reference and happens to be truthy, even if b() returns by value. However, -preview=rvaluerefparam requires implementations to always copy rvalue parameters, allowing the result of function calls to be subtly different from hand-inlined code.
struct S {
int i;
}
__gshared S gs = S(1);
ref S getRef() => gs;
S getVal() => gs;
int inc(ref S s) => ++s.i;
void main()
{
import core.stdc.stdio;
// modifies a copy of gs
inc(getRef().i ? getRef() : getVal());
printf("%d\n", gs.i);
// modifies gs
++((getRef().i ? getRef() : getVal()).i);
printf("%d\n", gs.i);
}
In the code above, the inc() call copies its argument and has no side-effect on gs. However, after several versions, the maintainer decides that getVal() should be ref to improve performance. Such a tiny change suddenly changes getRef().i ? getRef() : getVal() into an lvalue, and the code now prints 2 3 instead of 1 2.
The wording in https://github.com/dlang/dlang.org/pull/3924 is not clear about the situation. That all reference semantics is lost when passing an rvalue should be emphasized.
(This is all with rvaluerefparam enabled.) Maybe a solution is to introduce a third value category lrvalue that is the result of a ternary expression with an lvalue and rvalue branch. It is the same as rvalue for most things, but is an error when passed to a ref parameter or used to initialize a ref variable.
I've looked into this; it's a tough bug to solve, and I'm not 100% sure it should be. Current behaviour is reasonable.
- isLValue for the ternary operator requires both lhs and rhs to be lvalues, but really it should only require one (this will need to be opt-in for arguments).
- Function call where it does the rvaluerefparam stuff, doesn't rewrite into the tree like with the ternary operator.
As a former lisp programmer I was personally expecting setf semantics here i.e. each branch keeps its own ref-ness. I'm rewriting NRVO code in this vein to comply with C++ prvalue semantics (too busy to finish them now).
Maybe this should be discussed in the forum before being enabled by default, I guess people from C++ background are OK with the current (argument passing, but not RVO) behavior. I'm quite the opposite, hate this argument passing behavior but OK with ternary breaking RVO (still want to fix RVO though because of C++ interop).
In D the way ternary operator works when both true/false branches are by-ref:
*(condition ? &a : &b)
What should be happening with the preview switch:
*(condition ? &a : (T temp = b, &temp))
That's right. Dunno if the community thinks the fix's worth the effort.