Thread overview
traits: how to split parametrized type into basic type and parameters
Feb 13, 2011
Martin Kinkelin
Feb 13, 2011
Martin Kinkelin
Feb 14, 2011
Philippe Sigaud
Feb 14, 2011
Martin Kinkelin
Feb 14, 2011
Philippe Sigaud
Feb 14, 2011
Martin Kinkelin
Feb 14, 2011
Philippe Sigaud
February 13, 2011
Hi,

I'm implementing a generic Vector!(uint d, T) struct (wrapping a T[d] array).
For readability:
alias Vector!(4u,float) float4;
alias Vector!(4u,int)   int4;

I'd like to be able to cast a float4 variable explicitly to an int4
(component-wise casting):
auto f4 = float4(1,2,3,4);
auto i4 = cast(int4) f4;

So when implementing opCast!NewType() in Vector!(d,T), I need a signature
constraint: NewType must be a Vector!(d2,T2), with d2 == d && isNumeric!T2.
The problem is that I don't know how to split NewType into the basic type
(Vector) and its parameters ({uint d2, T2}) as I didn't find anything related
in the std.traits documentation. I currently use a custom casting method, but
am not happy with it (I prefer the clean casting syntax preceding the expression):
Vector!(d,T2) castTo(T2)() if (isNumeric!T2) { ... }
// e.g., auto i4 = f4.castTo!int();

Thanks in advance for any hints. Already falling in love with D after 2 days of experimenting :)
February 13, 2011
I realize a parametrized constructor would be a better option:
this(T2)(const(Vector!(d,T2)) rhs) if (isNumeric!T2) { ... }
// e.g., auto i4 = int4(f4);

but I'd still prefer the casting syntax if possible.
February 14, 2011
Hi Martin,

> I'm implementing a generic Vector!(uint d, T) struct (wrapping a T[d] array).
> For readability:
> alias Vector!(4u,float) float4;
> alias Vector!(4u,int)   int4;
>
> I'd like to be able to cast a float4 variable explicitly to an int4
> (component-wise casting):
> auto f4 = float4(1,2,3,4);
> auto i4 = cast(int4) f4;
>
> So when implementing opCast!NewType() in Vector!(d,T), I need a signature
> constraint: NewType must be a Vector!(d2,T2), with d2 == d && isNumeric!T2.
> The problem is that I don't know how to split NewType into the basic type
> (Vector) and its parameters ({uint d2, T2}) as I didn't find anything related
> in the std.traits documentation.

OK. First, to get access to a template parameters, the template must expose them by aliasing:

template Vector(uint d, T) if (isNumeric!T)
{
    alias d dim;
    alias T Type;

    T[d] values;
}

Now, d and T are externally accessible :

alias Vector!(4, float) Float4;
static assert(Float4.dim == 4);
static assert(is(Float4.Type == int));

Now, there is no built-in way to see if a type is a template instantiation. A solution is to make a templated function do the work for you. Here we go:

void vectorCheck(int d, T)(Vector!(d, T) v) {}

vectorCheck can be instantiated only if passed a Vector!(someDim, SomeType). I used 'int d' due to some bugs with 'uint d'.

Now, __traits offers the nifty 'compiles' instruction that checks at compile-time if a piece of code is valid D code. So we can create a template that checks if a type is indeed a Vector!(... , ...)

template isVector(T)
{
    static if (__traits(compiles, {
                                                   void
checkVector(int d, T)(Vector!(d,T) v) {} // define checkVector here

checkVector(T.init); // use it with a value of type T: T.init
                                               })) // if the previous
code compiles, then T is a Vector!(...,...)
        enum bool isVector = true;
    else
        enum bool isVector = false;
}

So, given a type T, isVector!T is true iff T is a Vector. Note that this template could easily be generalized: isInstanceOf!(someTemplateName, SomeType)

Now, Vector V1 can be cast into a Vector V2 iff
* V1.dim == V2.dim
* is(V1.Type : V2.Type)   I used the U : V syntax here, which means U
is a kind of V, or U derive from V or U can be cast into a V.

Which gives us the last building block:

template canBeCastInto(V1, V2) if (isVector!V1 && isVector!V2)
{
    static if ((V1.dim == V2.dim) && is(V1.Type : V2.Type)) // We know
V1 and V2 are Vectors, we can use V1.dim and V1.Type
        enum bool canBeCastInto = true;
    else
        enum bool canBeCastInto = false;
}


Vector!(4,int) v1;
Vector!(4,float) v2;

assert(   canBeCastInto!( typeof(v1), typeof(v2) )); // from int to float, yes
assert( ! canBeCastInto!( typeof(v2), typeof(v1) )); // no cast from
float to int

Of course, you can relax the type condition at your leisure. I mean, you can cast floats into ints if you want to. Also, you could cast small vectors into longer ones, putting the residual coordinates to Type.init.


Philippe
February 14, 2011
Hi Philippe,

thank you very much! I added your isVector!T template as well as the two aliases. opCast!NewType is implemented like this:

V opCast(V)() if (isVector!V)
// parameters {d2,T2} of V are checked when instantiating V: d2 >= 1 && isNumeric!T2
{
    V r;
    foreach (i; 0 .. (d < V.dim ? d : V.dim))
        r._data[i] = cast(V.Type) _data[i];
    return r;
}

thereby allowing to cast to arbitrary component types as well as to arbitrary dimensions. Works like a charm:

auto f3 = float3(1,2,3);
assert((cast(double2) f3) == double2(1,2));
assert((cast(int3) f3) == int3(1,2,3));
assert((cast(int4) f3) == int4(1,2,3,0));
February 14, 2011
On Mon, Feb 14, 2011 at 13:09, Martin Kinkelin <noone@spam.com> wrote:
> Hi Philippe,
>
> thank you very much!

You're welcome.

> I added your isVector!T template as well as the two aliases. opCast!NewType is implemented like this:
>
> V opCast(V)() if (isVector!V)
> // parameters {d2,T2} of V are checked when instantiating V: d2 >= 1 && isNumeric!T2
> {
>    V r;
>    foreach (i; 0 .. (d < V.dim ? d : V.dim))

Nice one, the compile-time ternary operator.

>        r._data[i] = cast(V.Type) _data[i];
>    return r;
> }

Did you try without the cast? Since we know that _data components can be cast to r._data elements, the compiler should take care of that for you.


> thereby allowing to cast to arbitrary component types as well as to arbitrary dimensions. Works like a charm:
>
> auto f3 = float3(1,2,3);
> assert((cast(double2) f3) == double2(1,2));
> assert((cast(int3) f3) == int3(1,2,3));
> assert((cast(int4) f3) == int4(1,2,3,0));

You can also use cast(float2) f3 to get rid of the third coordinate.
That's cool.

You could also have a look at opAssign (for v1 = v2 expresions), and alias this.
Try putting:

alias _data this;

into your Vector code. That way a Vector will 'become' a T[d] when it makes sense. You gain direct access to the underlying coordinates:

v[2] instead of v._data[2]

This allows to use functions taking T[n] as an argument: pass them a Vector, it will expose its _data member. This should affect casting also.

Another D idiom would be to define an .as!(SomeType) inner template that does the casting. Some people do not like cast. Though, as you saw, opCast can tightly control what kind of casting you authorize.

struct Vector() {
(...)

auto as(Target)() if (isVector!Target && Target.dim < ... etc)
{
   Target target;
  (fill the target)
  return target;
}

Very close to your opCast, really. Usage would be very close also:

assert( f3.as!(double2) == double2(1,2) );
assert( f3.as!(int3)        == int3(1,2,3) );



Philippe
February 14, 2011
> Did you try without the cast? Since we know that _data components can be cast to r._data elements, the compiler should take care of that for you.

Nope, I didn't try that because, for instance, floats shouldn't be implicitly castable to ints.

> You can also use cast(float2) f3 to get rid of the third coordinate.
> That's cool.

Yep, in case you want a copy. Additionally, I defined a to!(int d2)
method which returns a reference to the lower-dimensional components
(downcasting):

ref Vector!(d2,T) to(int d2)()
    if (d2 <= d)
{
    return *(cast(Vector!(d2,T)*) &this);
}

allowing for:
auto f3 = float3(1,2,3);
f3.to!2() = float2(-1,-2);
assert(f3 == float3(-1,-2,3));

> You could also have a look at opAssign (for v1 = v2 expresions), and
> alias this.

Well, _data is private (and afaik I cannot modify a symbol's visibility via alias), so I overloaded the indexing operators (as well as most of the other operators as well). I only use _data here directly to aid the compiler and facilitate optimizations. But thanks for pointing 'alias ... this' out, I was looking for a concept similar to implicit type conversions in C++.

The vector data is actually defined like this in Vector!(d,T):

union
{
    private T[d] _data;
    public struct
    {
                           T x;
        static if (d >= 2) T y;
        static if (d >= 3) T z;
        static if (d >= 4) T w;
    }
}

So from the outside, components are accessed either via indexing or via x,y,z and w (analog to HLSL/GLSL). I also added some downcasting shortcuts via properties, e.g.:

auto f4 = float4(1,2,3,4);
auto r = f4.xyz + 2.0; // downcast f4 to float3 w/o copying, add double
assert(r == double3(3,4,5)); // element type of r is double :)


Martin
February 14, 2011
On Mon, Feb 14, 2011 at 22:09, Martin Kinkelin <noone@spam.com> wrote:

> So from the outside, components are accessed either via indexing or via x,y,z and w (analog to HLSL/GLSL). I also added some downcasting shortcuts via properties, e.g.:
>
> auto f4 = float4(1,2,3,4);
> auto r = f4.xyz + 2.0; // downcast f4 to float3 w/o copying, add double
> assert(r == double3(3,4,5)); // element type of r is double :)

We had some discussions on swizzling here, in the past. It makes for a nice use of opDispatch(string s).