April 07, 2021

On Tuesday, 6 April 2021 at 02:14:17 UTC, Paul Backus wrote:

>

On Tuesday, 6 April 2021 at 01:47:47 UTC, Q. Schroll wrote:

>

On Friday, 26 March 2021 at 15:12:04 UTC, Paul Backus wrote:

>

Of course the ideal would be to make ref itself into a type qualifier (ref(T)).

If you look at all the exceptions C++ has for its reference type constructor, you'll immediately see why Walter created ref as a storage class and not a type constructor. C++ reference types in many cases don't compose; e.g. vector<int&> isn't a thing.
If you really think about it, you don't need ref as a type constructor. Although allowing ref local variables wouldn't be harmful, I guess.

Here's what the generic identity function currently looks like in D:

auto ref T identity(T)(auto ref T arg)
{
    import core.lifetime: forward;
    return forward!arg;
}

Here's what the generic identity function would look like if ref were a type qualifier:

T identity(T)(T arg)
{
    return arg;
}

I rest my case.

In C++, the identity function is

template<typename T>
T&& identitiy(T&& arg)
{
    return std::forward<T>(arg);
}

if you want it to work like identity(arg).

TL;DR: C++ doesn't do it the simple way for a reason.

If you do

template<typename T>
T identitiy(T arg)
{
    return arg;
}

you get copies unless you call it identity<int&>(arg) and identity<int&&>(1) (which defeats the purpose, I guess.

Template type deduction alone (C++ has 4 different rule sets about type deduction) is complicated. One reason for stripping ref from the argument types unless they explicitly bind to ref parameters is that references should act like aliases:

int i;
int& r = i;
f(i);
f(r);

Here, f(r) should act the same as f(i) irrespective of how f is defined. f could take int, int&, const int&, or T, const T&, or T&& for some T that can implicitly constructed from int. For int&& or T&it wouldn't compile. References as type constructors are weird. And please note, this is what I immediately could come up with and I verified. There's stuff with function types and static array bounds that I remember have shenanigans with references, but I'd have to do some search for details.

The only thing D does wrong in this regard is not having forward and move in object.d.

April 07, 2021

On Wednesday, 7 April 2021 at 21:31:38 UTC, Q. Schroll wrote:

>

Template type deduction alone (C++ has 4 different rule sets about type deduction) is complicated. One reason for stripping ref from the argument types unless they explicitly bind to ref parameters is that references should act like aliases:

int i;
int& r = i;
f(i);
f(r);

Here, f(r) should act the same as f(i) irrespective of how f is defined. f could take int, int&, const int&, or T, const T&, or T&& for some T that can implicitly constructed from int. For int&& or T&it wouldn't compile. References as type constructors are weird.

References as type constructors are weird in C++ because of specific choices made by the designers of C++ (such as: "references should act like aliases"). There is nothing inherent to the concept of a reference type that implies this kind of weirdness. D does not have to make the same choices as C++, and ref(T) in D does not have to suffer from the issues that references suffer from in C++.

April 07, 2021
On 4/5/21 10:14 PM, Paul Backus wrote:
> On Tuesday, 6 April 2021 at 01:47:47 UTC, Q. Schroll wrote:
>> On Friday, 26 March 2021 at 15:12:04 UTC, Paul Backus wrote:
>>> Of course the ideal would be to make `ref` itself into a type qualifier (`ref(T)`).
>>
>> If you look at all the exceptions C++ has for its reference type constructor, you'll immediately see why Walter created `ref` as a storage class and not a type constructor. C++ reference types in many cases don't compose; e.g. `vector<int&>` isn't a thing.
>> If you really think about it, you don't need `ref` as a type constructor. Although allowing `ref` local variables wouldn't be harmful, I guess.
> 
> Here's what the generic identity function currently looks like in D:
> 
>     auto ref T identity(T)(auto ref T arg)
>     {
>         import core.lifetime: forward;
>         return forward!arg;
>     }
> 
> Here's what the generic identity function *would* look like if `ref` were a type qualifier:
> 
>     T identity(T)(T arg)
>     {
>         return arg;
>     }

I'm not so sure. Complications arise when you pass an lvalue to identity - is it to be considered of type T or ref T? There are pros and cons to each, which is why C++ chose to allow both; its unprecedented decltype(auto) was defined especially to be part of function return type when it is to transport refness out.

Same class of problems have caused other anomalies in C++, such as decltype(identifier) and decltype((identifier)) having different types.

The case is open and hot.

April 07, 2021
On 4/6/21 5:14 PM, Per Nordlöw wrote:
> On Tuesday, 6 April 2021 at 21:09:13 UTC, Paul Backus wrote:
>> For non-copyable types. It's actually needed in both cases--we would need DIP 1040 (or something similar) to get rid of it.
> 
> So let's help Walter getting DIP-1040 accepted then. :)
> 
> What else is forward needed for? The doc says
> 
> "Forwards function arguments while keeping `out`, `ref`, and `lazy` on
> the parameters."
> 
> Why can't the compiler do that for us?

Because sometimes you want the usual semantics, i.e. create a copy of the argument.

These things are difficult to automate. I don't think a simple solution exists.
April 07, 2021
On 4/7/21 5:31 PM, Q. Schroll wrote:
> On Tuesday, 6 April 2021 at 02:14:17 UTC, Paul Backus wrote:
>> On Tuesday, 6 April 2021 at 01:47:47 UTC, Q. Schroll wrote:
>>> On Friday, 26 March 2021 at 15:12:04 UTC, Paul Backus wrote:
>>>> Of course the ideal would be to make `ref` itself into a type qualifier (`ref(T)`).
>>>
>>> If you look at all the exceptions C++ has for its reference type constructor, you'll immediately see why Walter created `ref` as a storage class and not a type constructor. C++ reference types in many cases don't compose; e.g. `vector<int&>` isn't a thing.
>>> If you really think about it, you don't need `ref` as a type constructor. Although allowing `ref` local variables wouldn't be harmful, I guess.
>>
>> Here's what the generic identity function currently looks like in D:
>>
>>     auto ref T identity(T)(auto ref T arg)
>>     {
>>         import core.lifetime: forward;
>>         return forward!arg;
>>     }
>>
>> Here's what the generic identity function *would* look like if `ref` were a type qualifier:
>>
>>     T identity(T)(T arg)
>>     {
>>         return arg;
>>     }
>>
>> I rest my case.
> 
> In C++, the identity function is
> ```C++
> template<typename T>
> T&& identitiy(T&& arg)
> {
>     return std::forward<T>(arg);
> }
> ```
> if you want it to work like `identity(arg)`.
> 
> **TL;DR:** C++ doesn't do it the simple way for a reason.
> 
> If you do
> ```C++
> template<typename T>
> T identitiy(T arg)
> {
>     return arg;
> }
> ```
> you get copies unless you call it `identity<int&>(arg)` and `identity<int&&>(1)` (which defeats the purpose, I guess.
> 
> Template type deduction alone (C++ has 4 different rule sets about type deduction) is complicated. One reason for stripping `ref` from the argument types unless they explicitly bind to `ref` parameters is that references should act like aliases:
> ```C++
> int i;
> int& r = i;
> f(i);
> f(r);
> ```
> Here, `f(r)` should act the same as `f(i)` irrespective of how `f` is defined. `f` could take `int`, `int&`, `const int&`, or `T`, `const T&`, or `T&&` for some `T` that can implicitly constructed from `int`. For `int&&` or `T&`it wouldn't compile. References as type constructors are weird. And please note, this is what I *immediately* could come up with and I verified. There's stuff with function types and static array bounds that I remember have shenanigans with references, but I'd have to do some search for details.
> 
> The only thing D does wrong in this regard is not having `forward` and `move` in `object.d`.

Very well put, thank you.

April 08, 2021

On Thursday, 8 April 2021 at 02:16:31 UTC, Andrei Alexandrescu wrote:

>

On 4/6/21 5:14 PM, Per Nordlöw wrote:

>

On Tuesday, 6 April 2021 at 21:09:13 UTC, Paul Backus wrote:

>

For non-copyable types. It's actually needed in both cases--we would need DIP 1040 (or something similar) to get rid of it.

So let's help Walter getting DIP-1040 accepted then. :)

What else is forward needed for? The doc says

"Forwards function arguments while keeping out, ref, and lazy on
the parameters."

Why can't the compiler do that for us?

Because sometimes you want the usual semantics, i.e. create a copy of the argument.

These things are difficult to automate. I don't think a simple solution exists.

I definitely agree that the "simple solution" probably doesn't exist, however I am kind of partial to just leaving it to the caller (especially for out, I've found in some places). It conflicts inside me however, because I like very plastic interfaces - however it kills resolution questions like you raised (lvalue vs. reference to lvalue) above.

April 08, 2021

On Thursday, 8 April 2021 at 02:10:55 UTC, Andrei Alexandrescu wrote:

>

I'm not so sure. Complications arise when you pass an lvalue to identity - is it to be considered of type T or ref T? There are pros and cons to each, which is why C++ chose to allow both; its unprecedented decltype(auto) was defined especially to be part of function return type when it is to transport refness out.

In the hypothetical world where ref is a type qualifier, T and ref(T) are distinct types. So if the lvalue's type were T you'd get identity!T, and if its type were ref(T), you'd get identity!(ref(T)). If you had a non-ref lvalue that you wanted to pass by reference to a template function that takes an argument not declared as ref (or auto ref), you'd add an explicit conversion to ref(T) at the call site. None of this requires any special cases or complications.

Unfortunately, there's no way to get to this world without a fairly substantial breaking change. Currently, it's possible to use by-value and by-reference overloads to distinguish between rvalues and lvalues:

void fun(int n) { writeln("rvalue"); }
void fun(ref int n) { writeln("lvalue"); }

int a = 123;
fun(a); // lvalue
fun(456); // rvalue

Making ref into a type qualifier would cause the rvalue overload to be called in both cases, since it would be an exact match vs. a match by implicit conversion.

Realistically, this is probably too disruptive a change for too little benefit, so I expect the best we'll get is a library Ref!T type, like C++'s std::reference_wrapper. Fortunately D has the tools to make such a type fairly ergonomic to use.

April 09, 2021

On Wednesday, 7 April 2021 at 21:44:52 UTC, Paul Backus wrote:

>

On Wednesday, 7 April 2021 at 21:31:38 UTC, Q. Schroll wrote:

>

Template type deduction alone (C++ has 4 different rule sets about type deduction) is complicated. One reason for stripping ref from the argument types unless they explicitly bind to ref parameters is that references should act like aliases:

int i;
int& r = i;
f(i);
f(r);

Here, f(r) should act the same as f(i) irrespective of how f is defined. f could take int, int&, const int&, or T, const T&, or T&& for some T that can implicitly constructed from int. For int&& or T&it wouldn't compile. References as type constructors are weird.

References as type constructors are weird in C++ because of specific choices made by the designers of C++ (such as: "references should act like aliases"). There is nothing inherent to the concept of a reference type that implies this kind of weirdness. D does not have to make the same choices as C++, and ref(T) in D does not have to suffer from the issues that references suffer from in C++.

As Andrei said, in C++, apart from the decltype exception (which is justified), references behave as aliases. In D, it's the same, apart from __trais(isRef) which is also justified. What else do you expect from a reference object? Maybe I'm biased, but I don't see how other solutions than C++'s would be better.

April 09, 2021

On Friday, 9 April 2021 at 20:40:54 UTC, Q. Schroll wrote:

>

As Andrei said, in C++, apart from the decltype exception (which is justified), references behave as aliases. In D, it's the same, apart from __trais(isRef) which is also justified. What else do you expect from a reference object? Maybe I'm biased, but I don't see how other solutions than C++'s would be better.

Hypothetically, if ref were a type qualifier, I'd expect it to behave the same way as D's other type qualifiers. We don't strip const or shared from types during template instantiation (the special case of const(T[])const(T)[] notwithstanding), so my baseline expectation is that we wouldn't strip ref either.

Maybe there's an argument to be made that this is a bad idea, and that you should always be able to replace an lvalue with a reference without any effect on the program's behavior. But I don't think you can take it as given that that's obviously the correct way to do references, just because that's what C++ does.

April 10, 2021

On Friday, 9 April 2021 at 21:17:03 UTC, Paul Backus wrote:

>

On Friday, 9 April 2021 at 20:40:54 UTC, Q. Schroll wrote:

>

As Andrei said, in C++, apart from the decltype exception (which is justified), references behave as aliases. In D, it's the same, apart from __trais(isRef) which is also justified. What else do you expect from a reference object? Maybe I'm biased, but I don't see how other solutions than C++'s would be better.

Hypothetically, if ref were a type qualifier, I'd expect it to behave the same way as D's other type qualifiers. We don't strip const or shared from types during template instantiation (the special case of const(T[])const(T)[] notwithstanding), so my baseline expectation is that we wouldn't strip ref either.

Maybe there's an argument to be made that this is a bad idea, and that you should always be able to replace an lvalue with a reference without any effect on the program's behavior. But I don't think you can take it as given that that's obviously the correct way to do references, just because that's what C++ does.

The "special case of const" is exactly the point: If you have a const(T[]) ts object and you do

auto ts2 = ts;

then, typeof(ts2) is const(T)[]. The whole point of making a copy is that it (or at least the first layer) becomes independent and mutable. I think references should behave the same: Assigning a ref(T) to an auto variable should create a copy that (or at least the first layer) is independent of the source. Creating another reference is near-useless.
When it comes to parameter passing, binding by copy is expected to be the default and binding by reference a specialty. One reason being that binding by copy is more flexible, e.g. it allows (implicit) conversions.

I don't think it's obviously the right way because C++ does it that way, but it's a hint. I came to the conclusion myself; thinking of various alternatives, I could see obvious problems. C++'s references have (obvious?) problems, so it's not like it's perfect. IMO, ref as a storage class is the Right Thing. D falls short in some places:

  • Allowing ref for local variables would be valuable at times. There's no actual gain in not allowing them. Some stuff like postfix operators even lower to ref local variables.
  • One cannot directly express ref returning delegate or function pointer types on many occasions, e.g. neither is(DG == ref int delegate()) nor is(DG == int delegate() ref) compile. Also, one cannot directly specify ref returning delegate or function pointer types in parameter lists. In both cases, an alias has to be defined.

If ref were a type constructor, these would be non-issues for sure. But we're nowhere near requiring it to be one to solve these.

1 2 3 4 5 6
Next ›   Last »