Thread overview
Explicit this for methods
Jul 23
vit
Jul 23
vit
Jul 23
vit
July 23

Explicit this allow manipulating of this type without interacting with function attributes and without need to create template function or string mixin-s.

Syntax:

  • explicit this must be first parameter of non-static method
  • name is not identifier but keyword this.
  • explicit this doesn't support all parameter attributes like ref, out, in and lazy, but attributes like scope and return can be supported.

Why?:

  • Type qualifiers can be in position of function attributes when this type qualifier need to be specified. Conditional manipulation of function attributes at compile time is very hard but manipulating types is much easer.
  • this template parameter can by replaced with explicit this + normal template parameter.
  • easer distinction between qualifiers/attributes for this and other function attributes like pure nothrow @nogc ref

examples:

class C{


	//this 3 methods are same:
	abstract void test1();			//method with implicit this
	abstract void test1(this);		//method with explicit this with qualifier (mutable)
	abstract void test1(C this);	//method with explicit this with type (is(Unqual!T == C))
	
	//this 3 methods are same:
	abstract void test2()const pure;		//method with implicit const this
	abstract void test2(const this)pure;	//method with explicit this with qualifier
	abstract void test2(const C this)pure;	//method with explicit this with type (is(Unqual!T == C))
	
	//this 3 methods are same:
	void test3(this This)()const pure{}		//template method with implicit this with this template parameter
	void test3(this This)(const this)pure{}	//template method with this   template parameter and explicit this with qualifier
	void test3(This)(const This this)pure{}	//template method with normal template parameter and explicit this with type (is(Unqual!T : C))
	

	const{	//can not be conditionaly specified at compile time
		abstract void foo();
		abstract void foo(int i);
		abstract void foo(string str);
	}
	
	alias FooThis = const(C);	//can be conditionaly specified at compile time
	abstract void foo2(FooThis this);
	abstract void foo2(FooThis this, int i);
	abstract void foo2(FooThis this, string str);
}

struct S{

	//this 2 methods are same:
	ref S test2() @safe scope return pure{return this;}
	ref S test2(scope return this)@safe {return this;}
	//ref S test2(ref return this)@safe {return this;}

}

One problem is postblit constructor:


class C{

	//this 3 ctors are same:
	this();				//constructor with implicit this
	this(this);			//AMBIGUOUS: conflict with postblit
	this(C this);		//constructor with explicit this with type (is(Unqual!T == C))
	
	//this 3 ctors are same:
	this()const;		//constructor with implicit const this
	this(const this);	//AMBIGUOUS: conflict with postblit
	this(const C this);	//constructor with explicit this with type (is(Unqual!T == C))
}

Explicit this is optional, non-static method without explicit this has still implicit this.

Explicit this can have other parameter attributes like:

  • scope
  • return scope
  • scope return

In the future explicit this can allow making lvalue this:

struct S{
	void test1(ref this)pure;	//callable only on lvalue?
}

What you think about this idea?

July 23

On Tuesday, 23 July 2024 at 11:07:54 UTC, vit wrote:

>

Explicit this allow manipulating of this type without interacting with function attributes and without need to create template function or string mixin-s.

Reminds me of C++23’s explicit object parameters. In C++, using D lingo, this was added as a parameter storage class. The disadvantage is that in C++, the instance pointer (normally this) must have a name different from this.

>

Syntax:

  • explicit this must be first parameter of non-static method
  • name is not identifier but keyword this.
  • explicit this doesn't support all parameter attributes like ref, out, in and lazy, but attributes like scope and return can be supported.

Why?:

  • Type qualifiers can be in position of function attributes when this type qualifier need to be specified. Conditional manipulation of function attributes at compile time is very hard but manipulating types is much easier.
  • this template parameter can by replaced with explicit this + normal template parameter.
  • easier distinction between qualifiers/attributes for this and other function attributes like pure nothrow @nogc ref

examples:

class C{
[…]
	abstract void test2(const C this)pure;	//method with explicit this with type (is(Unqual!T == C))

What is T? Did you intend Unqual!(typeof(this))?

>
	
	//this 3 methods are same:
	void test3(this This)()const pure{}		//template method with implicit this with this template parameter
	void test3(this This)(const this)pure{}	//template method with ```

The semantics of the one above is unclear.

>
[…]
	const{	//can not be conditionaly specified at compile time
		abstract void foo();
		abstract void foo(int i);
		abstract void foo(string str);
	}

“can not be conditionaly specified at compile time” No idea what that means.

>
	alias FooThis = const(C);	//can be conditionaly specified at compile time
	abstract void foo2(FooThis this);
	abstract void foo2(FooThis this, int i);
	abstract void foo2(FooThis this, string str);
}

struct S{

	//this 2 methods are same:
	ref S test2() @safe scope return pure{return this;}
	ref S test2(scope return this)@safe {return this;}
	//ref S test2(ref return this)@safe {return this;}

}

One problem is postblit constructor:


class C{

	//this 3 ctors are same:
	this();				//constructor with implicit this
	this(this);			//AMBIGUOUS: conflict with postblit
	this(C this);		//constructor with explicit this with type (is(Unqual!T == C))
	
	//this 3 ctors are same:
	this()const;		//constructor with implicit const this
	this(const this);	//AMBIGUOUS: conflict with postblit
	this(const C this);	//constructor with explicit this with type (is(Unqual!T == C))
}

Aren’t postblits deprecated? If not, the conflict with postblits can be rectified with this(const typeof(this) this).

>

[…]

Explicit this can have other parameter attributes like:

  • scope
  • return scope
  • scope return
    Logical.
>

In the future explicit this can allow making lvalue this:

struct S{
	void test1(ref this)pure;	//callable only on lvalue?
}

There’s a bigger issue with structs. For classes, binding this is by value, i.e. the class reference is copied. For structs, this is a reference to the object. However, unlike with ref, of course this by-reference binding can bind rvalues.

I’d do a complete 180° here: Allow ref this for structs and allow it to bind rvalues. A non-ref this parameter binds by copy. (For classes, no big deal, for structs, big difference.)

If we had @rvalue ref and @universal ref, I’d say use those and ref means lvalues only.

It may be a stupid question, but why allow the type of the this parameter to be specified? It only makes sense for templates. There, the semantics could just say that this simply has the type of the object it’s called on, i.e. explicit this implies template this.

Another question: Can this parameters be used to bind by pointer? That could actually be useful for handling null. I.e. if I have S* ptr for some struct S, a void f(S* this) could bind to ptr.f() and handle the case that ptr is null.

July 23

On Tuesday, 23 July 2024 at 11:42:03 UTC, Quirin Schroll wrote:

>

...

This dip idea allow this:

    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);
    }

instead of:

    template C(bool const_){

        class C{
            static if(const_){
                abstract void test1()const{return test1_impl(this);}
                abstract void test2(int i)const{test2_impl(this, i);}
                abstract void test3(string str)const{test3_impl(this, str);}
            }
            else{
                abstract void test1(){return test1_impl(this);}
                abstract void test2(int i){test2_impl(this, i);}
                abstract void test3(string str){test3_impl(this, str);}
            }
        }

        static if(const_)
        	alias This = const C;
        else
            alias This = C;

        void test1_impl(This self){/*body 1*/}
        void test2_impl(This self, int i){/*body 2*/}
        void test3_impl(This self, string str){/*body 3*/}
    }

Other example are copy constructors for ptr wrapper (like rc ptr, shared ptr, ...):


    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;
        }

    }

instead of:


    struct Ptr(T){
        private void* ptr;

        static foreach(alias Rhs; AliasSeq!(Ptr, const Ptr, immutable Ptr))
        {

            static if(is(CopyTypeQualifiers!(Rhs, T*) : T*))
                this(ref Rhs rhs)@trusted{
                    this.ptr = cast(typeof(this.ptr))rhs.ptr;
                }
            else
                @disable this(ref Rhs rhs)@trusted;

            static if(is(CopyTypeQualifiers!(Rhs, T*) : const(T*)))
                this(ref Rhs rhs)@trusted const{
                    this.ptr = cast(typeof(this.ptr))rhs.ptr;
                }
            else
                @disable this(ref Rhs rhs)@trusted const;

            static if(is(CopyTypeQualifiers!(Rhs, T*) : immutable(T*)))
                this(ref Rhs rhs)@trusted immutable{
                    this.ptr = cast(typeof(this.ptr))rhs.ptr;
                }
            else
                @disable this(ref Rhs rhs)@trusted immutable;
        }

    }

It simplify code and make forwarding parameters unnecessary.

> >
class C{
[…]
	abstract void test2(const C this)pure;	//method with explicit this with type (is(Unqual!T == C))

What is T? Did you intend Unqual!(typeof(this))?

Yes but typeof(this) in this context in unclear too. :) (it is type of aggregate type in witch is method, not type of this parameter)

July 23

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:

>

...

>
class C{
[…]
	abstract void test2(const C this)pure;	//method with explicit this with type (is(Unqual!T == C))

What is T? Did you intend Unqual!(typeof(this))?

Yes but typeof(this) in this context in unclear too. :) (it is type of aggregate type in witch is method, not type of this parameter)

I was wrong, this:

is(Unqual!(typeof(this)) == typeof(aggregate type in which is method located))
July 24
For type state analysis I do have a need to introduce ``this`` and ``return`` as function parameters.

However they would not be mandatory.

I can however see ``this`` having a variable name specified that overrides the implicit ``this`` pointer.

If you want to force it to be mandatory you could implement a check in DScanner, however I do not think D itself would have it. I would be against such a requirement.

I suspect that there might be some overlap here that would be beneficial to the language.

I do want to emphasize we must not introduce this to for similarity to other languages, it must be introduced to solve problems that D has.

July 25

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:

    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:

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);
}
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, ...):

    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;
        }

    }
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, 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 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:

// 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 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:

// 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:

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):

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.

July 27

On Tuesday, 23 July 2024 at 11:07:54 UTC, vit wrote:

>

Explicit this allow manipulating of this type without interacting with function attributes and without need to create template function or string mixin-s.

Syntax:

  • explicit this must be first parameter of non-static method
  • name is not identifier but keyword this.

Agree with all of this.

>
  • explicit this doesn't support all parameter attributes like ref, out, in and lazy, but attributes like scope and return can be supported.

To be more precise, any attribute or storage class that is currently specified as applying to the this parameter when used on a method should be supported.

Personally, I would also require the this parameter of a struct method to be declared as ref, since this is always passed by reference. (An explicit ref is not needed for class methods, because classes are reference types.)

>
  • this template parameter can by replaced with explicit this + normal template parameter.

I'm not sure allowing the user to specify the type of the this parameter is a great idea. Everything else in this proposal is just a new syntax for existing behavior; this would add entirely new behavior to the language.

July 27

On Saturday, 27 July 2024 at 00:08:19 UTC, Paul Backus wrote:

>

On Tuesday, 23 July 2024 at 11:07:54 UTC, vit wrote:

>

Explicit this allow manipulating of this type without interacting with function attributes and without need to create template function or string mixin-s.

Syntax:

  • explicit this must be first parameter of non-static method
  • name is not identifier but keyword this.

Agree with all of this.

>
  • explicit this doesn't support all parameter attributes like ref, out, in and lazy, but attributes like scope and return can be supported.

To be more precise, any attribute or storage class that is currently specified as applying to the this parameter when used on a method should be supported.

Personally, I would also require the this parameter of a struct method to be declared as ref, since this is always passed by reference. (An explicit ref is not needed for class methods, because classes are reference types.)

>
  • this template parameter can by replaced with explicit this + normal template parameter.

I'm not sure allowing the user to specify the type of the this parameter is a great idea. Everything else in this proposal is just a new syntax for existing behavior; this would add entirely new behavior to the language.

But ref means that it must me an lvalue.

A decision must be made if this proposal wants an alternative syntax for non-static member functions or if it wants to add something to the language that isn't expressible in the current state.

My argument is that adding explicit this has a low chance of adoption if it adds nothing that can't already be expressed. It's best rationale, … I have it spelled out in quite some detail in my previous post – not gonna repeat that.
If we were discussing how to add non-static member functions, that proposal would be a good contender against implicit this, but we already have implicit this, so if this isn't going to add something, it has little chance of getting through. It's not good for a language if there are two competing ways of doing things which are roughly equal and low in complexity. That can lead to style wars. It would be a completely different story if this were to propose deprecating implicit this.