Search code examples
d

signature constraint for generic types


struct S(int a, int b) { }

void fun(T)(T t) { }

I want fun to work with S only. What would the signature constraint look like?

I can't make fun a member of S, and with void fun(T)(T t) if(is(T : S)) { } I get Error: struct t1.S(int a,int b) is used as a type


Solution

  • S is not a type. It's a template for a type. S!(5, 4) is a type. It's quite possible that different instantiations of S generate completely different code, so the definition of S!(5, 4) could be completely different from S!(2, 5). For instance, S could be

    struct S(int a, int b)
    {
        static if(a > 3)
            string foo;
    
        static if(b == 4)
            int boz = 17;
        else
            float boz = 2.1;
    }
    

    Note that the number and types of the member variables differ such that you can't really use an S!(5, 4) in place of an S!(2, 5). They might as well have been structs named U and V which weren't templatized at all for all of the relation that they really have to one another.

    Now, different instantiations of a particular template are generally similar with regards to their API (or they probably wouldn't have been done with the same template), but from the compiler's perspective, they have no relation with one another. So, the normal way to handle it is to use constraints purely on the API of the type and not on its name or what template it was instantiated from.

    So, if you expect S to have the functions foo, bar, and foozle, and you want your fun to use those functions, then you'll construct a constraint that tests that the type that's given to fun has those functions and that they work as expected. For instance

    void fun(T)(T t)
        if(is({ auto a = t.foo(); t.bar(a); int i = t.foozle("hello", 22);}))
    {}
    

    Then any type which has a function called foo which returns a value, a function named bar which may or may not return a value and which takes the result of foo, and a function named foozle which takes a string and an int and returns an int will compile with fun. So, fun is far more flexible than if you had insisted on it taking only instantiations of S. In most cases, such constraints are separated out into separate eponymous templates (e.g. isForwardRange or isDynamicArray) rather than putting raw code in an is expression so that they're reusable (and more user friendly), but expressions like that are what such eponymous templates use internally.

    Now, if you really insist on constraining fun such that it only works with instantiations of S, then there are two options that I'm aware of.

    1. Add a declaration of some kind which S always has and you don't expect any other type to have. For instance

    struct S(int a, int b)
    {
        enum isS = true;
    }
    
    void fun(T)(T t)
        if(is(typeof(T.isS)))
    {}
    

    Note that the actual value of the declaration doesn't matter (nor does its type). It's the simple fact that it exists that you're testing for.

    2. The more elegant (but far less obvious solution) is to do this:

    struct S(int a, int b)
    {
    }
    
    void fun(T)(T t)
        if(is(T u : S!(i, j), int i, int j))
    {}
    

    is expressions have a tendancy to border on voodoo once they get very complicated, but the version with commas is precisely what you need. The T u is the type that you're testing and an identifier; the : S!(i, j) gives the template specialization that you want T to be an instantiation of; and the rest is a TemplateParameterList declaring the symbols which are used in the stuff to the left but which haven't previously been declared - in this case, i and j.