Explicit this for methods

Quirin Schroll qs.il.paperinik at gmail.com
Thu Jul 25 20:02:06 UTC 2024


On Tuesday, 23 July 2024 at 14:00:30 UTC, vit wrote:
> On Tuesday, 23 July 2024 at 11:42:03 UTC, Quirin Schroll wrote:
>> ...
>
> This dip idea allow this:
>
> ```d
>     template C(bool const_)
>     {
>         class C{
>             abstract void test1(This this){/*body 1*/}
>             abstract void test2(This this, int i){/*body 2*/}
>             abstract void test3(This this, string str){/*body 
> 3*/}
>         }
>
>         static if(const_)
>             alias This = const typeof(this);
>         else
>             alias This = typeof(this);
>     }
> ```

Something like that would easily be possible if `const` could be 
generated by a mixin.

Even without:
```d
class C(bool const_)
{
     import std.conv : text;
     private enum q = const_ ? "const" : "";
     mixin(iq{
         abstract void test1() $(q);
         abstract void test2(int i) $(q);
         abstract void test3(string str) $(q);
     }.text);
}
```

```d
class C(bool const_)
{
     import std.format : format;
     pragma(msg, q{
         abstract void test1() %1$s;
         abstract void test2(int i) %1$s;
         abstract void test3(string str) %1$s;
     }.format(const_ ? "const" : ""));
}
```

>[…]
>
> Other example are copy constructors for ptr wrapper (like rc 
> ptr, shared ptr, ...):
>
> ```d
>     template Ptr(T){
>         private void* ptr;
>
>         static foreach(alias Rhs; AliasSeq!(Ptr, const Ptr, 
> immutable Ptr))
>         static foreach(alias Lhs; AliasSeq!(Ptr, const Ptr, 
> immutable Ptr))
>         {
>             static if(is(CopyTypeQualifiers!(Rhs, T*) : 
> CopyTypeQualifiers!(Lhs, T*)))
>                 this(Lhs this, ref Rhs rhs)@trusted{
>                     this.ptr = cast(typeof(this.ptr))rhs.ptr;
>                 }
>             else
>                 @disable this(Lhs this, ref Rhs rhs)@trusted;
>         }
>
>     }
> ```

```d
struct Ptr(T)
{
     import std.conv : text;

     private void* ptr;

     static foreach(int i, LQ; ["", "const", "immutable"])
     static foreach(int j, RQ; ["", "const", "immutable"])
     {
         static if(is(mixin(RQ, " T")* : mixin(LQ, " T")*))
         {
             mixin(iq{
                 this(ref $(RQ) Ptr rhs) $(LQ) @trusted
                 {
                     this.ptr = cast(typeof(this.ptr))rhs.ptr;
                 }
             }.text);
         }
         else
         {
             mixin(iq{
                 this(ref Ptr!($(RQ) T) rhs) $(LQ) @trusted 
@disable;
             }.text);
         }
     }
}
```
It might even be clearer to manually consider every qualifier for 
`T` and then write specific constructors. Using `inout`, it’s 
possible to greatly simplify these, e.g. for any `T`, `this(inout 
Ptr) const` works. In particular, your and my implementation 
can’t handle initializing a `Ptr!(const int)` by a `Ptr!int`.

If you provide examples, make them as real-world as possible, or 
say they are intentionally simplified or contrived.

---

Again, you’re not using the best arguments for explicit `this`. 
Simplifying implementations can be an argument, but you have to 
provide evidence for ***significant*** simplification 
opportunities. Consider the rationales presented in 
[P0847](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html), the proposal to add explicit object parameters to C++23. If you write this DIP, you’d have to elaborate on it in the *Prior Work* section anyway.

The paper’s arguments that also apply to D:
- 
[CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) is easy mode now
- pass by value made possible
- recursive lambdas are possible

The CRTP is a lot weaker as an argument for D because D has no 
struct inheritance and class inheritance is always virtual, i.e. 
D has no option for static polymorphism. The best approximation 
is mixin templates. Taken from 
[Wikipedia](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern#Deducing_this):
```cpp
// C++ code
struct chef_base
{
     template <typename Self>
     void signature_dish(this Self&& self)
     {
         self.cook_signature_dish();
     }
};

struct cafe_chef : chef_base
{
     void cook_signature_dish() {}
};
```
The D way is:
```d
// D code
mixin template ChefBase()
{
     void signatureDish()
     {
         this.cookSignatureDish();
     }
};

struct CafeChef
{
     mixin ChefBase!();
     void cookSignatureDish() {}
};
```

D-specific ones:
- lvalue can be forced
- lvalue and rvalue can be distinguished

Code quadruplication is much less of an issue in D because D has 
`inout`, `static foreach` and string mixins.
Recursive lambdas in D are a half-issue in D:
```d
// okay:
alias factorial = function int (int i) => i <= 1 ? 1 : 
factorial(i - 1) * i;

// error:
auto factorial = function int (int i) => i <= 1 ? 1 : factorial(i 
- 1) * i;
```
And if you don’t assign the lambda to anything directly (pass it 
as a template/function argument), you’re out of luck in D. 
Lambdas are quite fundamentally different in C++ and D. Also, you 
run into syntax issues if you insist on `this` being the 
“identifier” because a lambda can capture its context’s `this`. 
My bet is this is one reason why in C++, `this` is used as a 
storage class and requires an identifier. We could make an 
exception for lambdas – after all, lambdas’ parameter lists are 
special anyways:
```d
void higherOrderFunction(int function(int) fp);
higherOrderFunction((this factorial, int i) => i <= 1 ? 1 : 
factorial(i - 1) * i);
```
The factorial lambda is a 1-ary function, not a 2-ary function. A 
recursive `this` parameter is just a way to express the function 
to itself.

Thinking about it, this could actually be generalized: `this` as 
a storage class adds a fake parameter that refers to the function 
itself. For a normal function, that could be allowed, but useless 
(and annoying, because they require you to spell their type):
```d
void f(this void function() fp) { fp(); f(); } // both calls do 
the same
struct S
{
     void g(this void delegate() dg) { dg(); g(); } // both calls 
do the same
}
```
But it really only makes sense for lambdas.

About requiring lvalues, the situation is different in D than for 
C++. In C++03, if a member function exposes a reference to a data 
member, it’s easy to hold on to a dangling reference if the 
member function is called on an rvalue. So, distinguishing lvalue 
and rvalue objects, added in C++11, can make sense, where the 
rvalue overload returns the data member by move reference or 
moved value. (Then, calling the member function on rvalues is 
(quite) safe, on lvalues it’s still unsafe, but generally, you’ll 
get it right.) None of this is necessary in D because with 
DIP1000, `@safe` does some tracking and recognizes when a field 
reference outlive the object.

There might still be use-cases where a member function isn’t 
meaningful for lvalues/rvalues or should behave slightly 
differently for lvalues and rvalues. As for any function, 
rvalue-only ones can be done by providing both lvalue and rvalue 
overloads and `@disable`-ing the lvalue one.


More information about the dip.ideas mailing list