Thread overview
Enhanced UFCS
May 26
xoxo
May 26
Dennis
May 27
Kagamin
May 26
monkyyy
6 days ago
Quirin Schroll
May 26

Improving UFCS for Pointers to Structs

I’ve often found myself avoiding UFCS in D simply because I couldn’t use it consistently. The compiler forces me to duplicate functions when working with both values and pointers, which defeats a lot of the elegance UFCS is supposed to provide.

Here’s a minimal example:

struct Vector
{
    int data;
}

void add(ref Vector self, int value)
{
    self.data += value;
}

void main()
{
    Vector v;
    Vector* ptr = &v;

    v.add(1);     // Works
    ptr.add(1);   // Error: no matching function
}

The ptr.add(1) line fails because UFCS doesn’t automatically dereference the pointer to match the ref parameter in add. To make it work, I’d need to either call add(ptr, 1) or duplicate the function to handle Vector. Neither option feels ideal.

Suggestion

Could D support automatic UFCS for pointers to structs? That is, allow the compiler to transform ptr.add(1) into add(*ptr, 1) if the function takes a ref and ptr is a pointer to the expected type.

This would make UFCS much more usable without changing any behavior for existing code. Maybe this could be enabled via auto ref, inout ref, or something similar if implicit dereferencing is too risky to do universally.

Why This Matters

In Zig, for example, UFCS works seamlessly whether you have a value or a pointer — the compiler just figures it out. That kind of polish would be great to see in D as well.

Benefits

Less boilerplate

Cleaner, more ergonomic UFCS

Better parity with languages like Zig

No need to manually unwrap pointers just to get method-like syntax

May 26

On Monday, 26 May 2025 at 07:55:20 UTC, xoxo wrote:

>

Improving UFCS for Pointers to Structs

I’ve often found myself avoiding UFCS in D simply because I couldn’t use it consistently. The compiler forces me to duplicate functions when working with both values and pointers, which defeats a lot of the elegance UFCS is supposed to provide.

So the biggest caveat I see is if there are multiple options. I would say this counts as a conversion for purposes of overloading.

Looks good to me though, well stated.

-Steve

May 26

I've encountered this limitation as well, and like this idea of more uniformity. But as Steven said, care needs to be taken with overloads:

// foo.d
int f(S* x) => 2; // in an imported module

// bar.d
import foo;
int f(ref S x) => 1; // in this module

S* ptr;
void main()
{
    writeln(ptr.f); // 1 or 2?
}

On Monday, 26 May 2025 at 07:55:20 UTC, xoxo wrote:

>

Here’s a minimal example:

As of 2.111, you can do ref Vector ptr = v; instead of using a pointer, so perhaps you can use a different motivating example.

May 26

On Monday, 26 May 2025 at 07:55:20 UTC, xoxo wrote:

>

Improving UFCS for Pointers to Structs

I think overload resulation is a big old mess and even small asks will break stuff unexpectedly. This could be thrown to the bottom of the list of is 6(?) rules and then only stuff using compiles check would break, but that would reduce its scaling drasticly and would still be a fight.

also:

struct Vector
{
    int data;
}

void add(T)(ref T self, int value)//<--
{
    self.data += value;
}

void main()
{
    Vector v;
    Vector* ptr = &v;

    v.add(1);     // Works
    ptr.add(1);   // also works
}

Id suggest trying for either:

a) reusing the property keyword for ufcs functions to modify overload resulation(in my opinion the rules are exactly backwards)
b) adding a type specailization that matches for T and T*
c) ufcs perfers a named argument of "self"/"ufcs"/"this"

May 27

On Monday, 26 May 2025 at 14:18:36 UTC, Dennis wrote:

>

As of 2.111, you can do ref Vector ptr = v; instead of using a pointer, so perhaps you can use a different motivating example.

I think, it works only for local variables.

char*[] args;
args[0].strlen;
6 days ago

On Monday, 26 May 2025 at 07:55:20 UTC, xoxo wrote:

>

Improving UFCS for Pointers to Structs

My two cents:

The idea is generally good. Having to write (*ptr).f is annoying, considering it’s not necessary when f is a member of typeof(*ptr), only when f is a free function called via UFCS.

Some say it’s odd with overloading. In the imported module case, there are already overload sets in place.

module a;

struct S { }

void f(S*) { import std.stdio; writeln("a.f"); }
module b;

import a : S;

void f(S) { import std.stdio; writeln("b.f"); }
void main()
{
    import a, b;

    S* p = new S;
    p.f(); // Current behavior: Calls a.f
    (*p).f(); // Current behavior: Calls b.f
}

With your proposal, the first call becomes ambiguous. Overload resolution finds a.f and b.f and it can call both of them; because they belong to different overload sets, that’s an ambiguity error.

So, one thing to note is: Your DIP can break code. It’s not overly likely, but not so unlikely that you can blatantly assume it won’t cause problems.

Contrary to others, I don’t think overload resolution is a big concern though. Just define dereferencing in a UFCS call as an implicit conversion. It’s a special case, yes, but not too special.