Search code examples
templatesmetaprogrammingd

Template comparison changes inside template body


I have a struct Foo!T and a function that operates on any two Foo!T. I would expect such a function to be declared

void fun(U)(U a, U b) if (is(U : Foo!T, T...)) { }

However, it turns out that I can declare it as

void fun(U)(U a, U b) if (is(U : Foo)) { }

only if I declare it inside the body of Foo.

For example:

struct Foo(T) { 
    void fun1(U)(U b) if (is(U : Foo)) { } 
}

void fun2(U)(U a, U b) if (is(U : Foo)) { }

unittest {
    Foo!int f;
    f.fun1(f);
    f.fun2(f);
}

The above fails with struct d.Foo(T) is used as a type on the invocation of fun2. fun1, however, is fine.

Why is the constraint is(U : Foo) valid inside the body of Foo, but not outside?

Is the comparison is(U : Foo) inside the body of Foo equivalent to is(U : Foo!V, V...) outside the body of Foo?


Solution

  • Inside of a templated type, the name of the template refers to that particular instantiation of the template except when it's explicitly given a different instantiation. For instance,

    struct Foo(T)
    {
        pragma(msg, Foo.stringof);
    }
    pragma(msg, Foo.stringof);
    
    void main()
    {
        Foo!int foo1;
        Foo!string foo2;
    }
    

    prints

    Foo(T)
    Foo!int
    Foo!string
    

    The first pragma printed is the one right after the template, and the other two are generated when Foo is instantiated. This makes it so that you don't have to plaster Foo!T all over the place inside your struct declaration when you're referring to it (e.g. when returning it from a member function). This is particularly useful if the type has several template parameters instead of just one. But it does mean that if you want to refer to the general template, you either need to instantiate it with specific arguments - e.g. use Foo!int inside of Foo to refer to Foo!int regardless of what the current instantiaton is - or you need to use the dot operator to indicate that you want Foo from the outer scope. e.g.

    struct Foo(T)
    {
        pragma(msg, Foo.stringof);
        pragma(msg, .Foo.stringof);
    }
    pragma(msg, Foo.stringof);
    
    void main()
    {
        Foo!int foo1;
        Foo!string foo2;
    }
    

    prints

    Foo(T)
    Foo!int
    Foo(T)
    Foo!string
    Foo(T)
    

    So, when writing stuff like template constraints inside of a templated type, be aware that using Foo inside of struct Foo(T) or class Foo(T) will mean that specific instantiation and not the template itself.

    Also, if you're looking specifically to test whether U is an instantition of Foo, I'd suggest using std.traits.isInstanceOf - e.g. if(isInstanceOf(Foo, U)). Arguably, it really should be isInstantiationOf rather than isInstanceOf, but regardless, it does the job in a more idiomatic way than using naked is expressions.