[Issue 21065] the new operator should lower to a template function call in object.d

d-bugmail at puremagic.com d-bugmail at puremagic.com
Thu Jul 23 03:42:23 UTC 2020


https://issues.dlang.org/show_bug.cgi?id=21065

--- Comment #3 from Mathias LANG <pro.mathias.lang at gmail.com> ---
> the instantiations are limited by the number of calls to new in source code, which is a small fraction of the code in the project

That is a good point, however this report is about the downside of runtime
solution disadvantage. We should also consider the disadvantages that come with
a compile time solution: code duplication leading to bigger binaries, slowing
down compile time, potential cache thrashing due to the increased amount of
instructions. However, as explained after, those are not even the primary
concern with a variadic approach.

> Hmmm, this would be a problem. Can you please explain a bit?

Consider the following code:
```
class Klass { this (int value) {} }
void main ()
{
    scope c = new Klass(ulong.init);
}
```

This currently compiles just fine.
A naive implementation of `makeClassObject` would be (per your OP):
```
class Klass { this (int value) {} }
void main ()
{
    auto c = makeClassObject!Klass(ulong.init);
}
T makeClassObject (T, Args...) (auto ref Args args)
{
    return new T(args);
}
```

Which would give you:
```
foo.d(8): Error: constructor foo.Klass.this(int value) is not callable using
argument types (ulong)
foo.d(8):        cannot pass argument _param_0 of type ulong to parameter int
value
foo.d(4): Error: template instance foo.makeClassObject!(Klass, ulong) error
instantiating
```

This is because the function parameters are disconnected from their usage. We
let the compiler infer the template arguments based on what is given to the
function, then try to pass them to an overload set.

Another example that will fail is:
```
class Klass
{
    this (int value) {}
    this (ref int value) { assert(0); }
}
void main ()
{
    // scope c = new Klass(0);
    auto c = makeClassObject!Klass(0); // rvalue promoted to lvalue
}
```

Currently, this does not trigger the `assert`, but with the signature you
suggested, it does.

> As a matter of principle, the more compiler magic we need the more problems with the language we're sweeping under the rug. Both of these seem to be in that category.

There is nothing magic about the suggestion though, it relies on a very basic
principle: forwarding should use the same set of constraints.

Using variadics + auto ref as a way to provide a proxy for an overload set is a
common, yet fundamentally flawed approach, because it disables some of the
machinery in place for overload resolution (e.g. implicit conversion). This set
of rules the compiler applies when doing a call is well understood by
developers and very intuitive. But by using variadics, the proxy provides the
loosest of filter for arguments, then tries to fit those into a tighter filter.

Trying to fix this issue while retaining the variadics + auto ref approach
would require re-engineering some of the compiler machinery. In the case of the
`ulong.init`, a runtime switch will do it, while for `ref` one needs to inspect
the type of parameter to call the right function. There might be cases it's not
even possible (for example, mutable => immutable conversion when the argument
is a call to a strongly pure function).

This is an issue which can be seen in `Algebraic`, where it doesn't let you
pass a `const(int)` to an Algebraic accepting `int` (I mentioned it here:
https://forum.dlang.org/post/wvhlrgbtdbrvaubflabu@forum.dlang.org). Had it
generated an overload set based on the allowed types, this issue would be gone.

A basic PoC for `makeClassObject` would be this implementation:
```
import std.stdio;

class Klass
{
    this (int value) { writeln("rvalue"); }
    this (ref int value) { writeln("lvalue"); }
}
void main ()
{
    auto c = makeClassObject!Klass(0);
    int lvalue;
    auto d = makeClassObject!Klass(lvalue);
    auto e = makeClassObject!Klass(ulong.min);
}

template makeClassObject (T)
{
    import std.traits;
    static foreach (ovrld; __traits(getOverloads, T, "__ctor"))
    {
        T makeClassObject (Parameters!ovrld args)
        {
            scope ctor = &ovrld; // Need this to avoid promoting rvalues to
lvalues
            return ctor(args);
        }
    }
}
```

This *might* have issues with some storage classes, are those are usually
finicky to work with, but it will avoid most of the common (if not trivial)
issues an unbound variadic + auto ref templates will trigger.



TL;DR: If you want to forward to an overload set, do it by exposing an overload
set.

--


More information about the Digitalmars-d-bugs mailing list