Overloading based on attributes - is it a good idea?

Jonathan Marler johnnymarler at gmail.com
Wed May 29 02:45:37 UTC 2019


On Wednesday, 29 May 2019 at 01:14:04 UTC, Walter Bright wrote:
> On 5/28/2019 3:12 PM, Jonathan Marler wrote:
>> You are correct that call graphs are not acyclic, however, the 
>> inference of attributes only applies to templated-functions, 
>> not regular functions.  So we don't need to walk the entire 
>> graph to determine whether or not a template is allowed to use 
>> the GC, you only need to trace each template to it's nearest 
>> non-template function.
> Your algorithm will have to work for the worst case, which is 
> all template code. This is hardly unreasonable, as Phobos is 
> nearly entirely templates, with cyclical expansions. Worse, 
> it's not just attribute inference, you proposed different code 
> paths for different attributes, meaning the graph changes, too.
>
> It's a combinatorial explosion.

Well, I didn't propose an algorithm.  I was just providing a 
visual aid to your graph analogy. My suggesion has nothing to do 
with attribute inference on templates.  If it did then you are 
completely right that this would be a combinatorial explosion.  
The idea is actually pretty "dumb" at its core.  What's the 
simplest way to pass this information to a template?  Just add a 
template parameter.

foo(bool nogc, bool nothrow, bool pure,...)()
{
     // logic to do something different based on template 
parameters
}

Then we need a way infer these attributes when we aren't inside a 
template. You can do this today with things like:

foo!(__traits(compiles, new Object), __traits(compiles, throw new 
Object), ...)();

This function call is long, but the point of it is that this same 
call will pass the correct information based on whether the 
surrounding scope is nogc and/or nothrow. It infers the 
attributes before instantiating the template and passes the 
results to it.  But keep in mind, it is not inferring attributes 
for the template, it is only passing information about the caller 
to the template which it may or may not use.

But I think everyone can immediately see the problem with this.  
We can't add these parameters to every single template, and then 
add these __traits to every single template instantiation. There 
would be more boiler-plate than actual code!

So came the idea of "implicit template parameters".  The idea 
would be that any template could access a set of implicit 
variables to customize behavior.  And to prevent an exponential 
explosion of template instances, you would only consider implicit 
template parameters when they are used, i.e.

template foo()
{
     static if (__traits(requiresNothrow))
         auto foo() nothrow { ... }
     else
         auto foo() { ... }
}

void bar1() { foo(); }
void bar2() nothrow { foo(); }

When the compiler sees the `requiresNoThrow` trait being used 
inside a template, it notes that it is using one of the known 
implicit template parameters, meaning that it will be 
instantiated differently when the implicit "requiresNothrow" 
template parameter changes, just like it would if it was a normal 
template parameter.

So in this example, the two calls to foo in bar1 and bar2 
actually instantiate 2 versions of foo, even though they have the 
same "explicit template arguments", they have different "implicit 
template arguments".

To understand how this works, note that implicit and explicit 
template parameters work the same way.  So it would function just 
as if you had done this:

template foo(bool nothrow)
{
     static if (nothrow)
         auto foo() nothrow { ... }
     else
         auto foo() { ... }
}

void bar1()         { foo!(__traits(compiles, new Object))(); }
void bar2() nothrow { foo!(__traits(compiles, new Object))(); }

It's just that now you don't need the boilerplate.

Is it a good idea?  I don't know, but I think it's worth some 
thought.




More information about the Digitalmars-d mailing list