Templated vectors

In episode 71, Casey mentions that templatizing vectors would be another approach (not one that he would necessarily use) to implement them to avoid copy-pasting and duplicated code. I'm not a big template fan either, but I do use them on occasion when it makes sense, so I decided to templatize the vectors to see how it would come out. In case others are interested in this approach, here is what I came up with:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
template <int T>
union vec
{
    real32 e[T];
    
    real32& operator[](int index) { return e[index]; }
    const real32& operator[](int index) const { return e[index]; }
};

template<>
union vec<2>
{
    struct { real32 x, y; };
    real32 e[2];
};

template<>
union vec<3>
{
    struct { real32 x, y, z; };
    struct { real32 r, g, b; };
    struct { vec<2> xy; real32 pad_; };
    real32 e[3];
};

template<>
union vec<4>
{
    struct { real32 x, y, z, w; };
    struct { real32 r, g, b, a; };
    struct { vec<3> xyz; real32 pad_; };
    real32 e[4];
};

typedef vec<2> vec2_t;
typedef vec<3> vec3_t;
typedef vec<4> vec4_t;

#if 0
//!!! DANGER !!!
template <int T> vec<T>
Vec(real32 e, ...)
{
    vec<T> result;
    va_list args;
    va_start(args, e);
    
    result.e[0] = e;
    for (int i = 1; i < T; ++i) {
        result.e[i] = (real32)va_arg(args, real64);
    }
    
    va_end(args);
    
    return result;
}
#endif

template <int T, class... Args> vec<T>
Vec(Args... args)
{
    const int length = sizeof...(args);
    static_assert(length == T, "");
    vec<T> result = {static_cast<real32>(args)...};
    return result;
}

template <int T> vec<T>
operator*(real32 scalar, vec<T> v)
{
    vec<T> result;
    for (int i = 0; i < T; ++i) {
        result.e[i]  = scalar * v.e[i];
    }
    
    return result;
}

template <int T> vec<T>
operator*(vec<T> v, real32 scalar)
{
    vec<T> result = scalar * v;
    return result;
}

template <int T> vec<T>&
operator*=(vec<T>& v, real32 scalar)
{
    v = scalar * v;
    return v;
}

template <int T> vec<T>
operator-(vec<T> v)
{
    vec<T> result;
    for (int i = 0; i < T; ++i) {
        result.e[i] = -v.e[i];
    }
    
    return result;
}

template <int T> vec<T>
operator+(vec<T> A, vec<T> B)
{
    vec<T> result;
    for (int i = 0; i < T; ++i) {
        result.e[i] = A.e[i] + B.e[i];
    }
    
    return result;
}

template <int T> vec<T>&
operator+=(vec<T>& A, vec<T> B)
{
    A = A + B;
    return A;
}

template <int T> vec<T>
operator-(vec<T> A, vec<T> B)
{
    vec<T> result;
    for (int i = 0; i < T; ++i) {
        result.e[i] = A.e[i] - B.e[i];
    }
    
    return result;
}

template <int T> vec<T>&
operator-=(vec<T>& A, vec<T> B)
{
    A = A - B;
    return A;
}

template <int T> vec<T>
Hadamard(vec<T> A, vec<T> B)
{
    vec<T> result;
    for (int i = 0; i < T; ++i) {
        result.e[i] = A.e[i] * B.e[i];
    }
    
    return result;
}

template <int T> real32
Dot(vec<T> A, vec<T> B)
{
    real32 result = 0.f;
    for (int i = 0; i < T; ++i) {
        result += (A.e[i] * B.e[i]);
    }
    
    return result;
}

template <int T> real32
LengthSq(vec<T> v)
{
    real32 result = Dot(v, v);
    return result;
}

template <int T> real32
Length(vec<T> v)
{
    real32 result = Sqrt(LengthSq(v));
    return result;
}


One notable thing is that by using template specialization, you can in fact use member accesses like .x, .y, .r, .g, etc. That would have been a deal-breaker for me, because writing
1
velocity.x;

is obviously so much more preferable than
1
velocity.e[0];

or
1
velocity[0];


I also included the va_args version of the creator function as an alternative (particularly since some compilers may not yet support variadic templates), but it's marked danger for good reason. It doesn't offer any type-safety for the arguments after the first one, so writing
1
vec<3> aVec = Vec<3>(1.5f, 2.5f, 6);

will lead to trouble (i.e. when I tested it, the 6 turned into a 0 so I had {1.5, 2.5, 0}!).

While I haven't tested this, the for loops should be unrolled by any decent compiler since the bounds are known at compile time. All in all, it was pretty straightforward, but in the end I'm not sure this buys us a whole lot. Even though you would just have to add a function once in this version, it is less readable.

In any case, I'm going to keep in my version of HmH because, well.. I took the time to write it. :P

EDIT: I've only tested this using Clang.

Edited by Flyingsand on
This is awesome! :)

This really does seem like a best of both worlds solution, where you get the good things you get with templates (not having to write multiple near-identical copies of functions for vec2, vec3, vec4), without sacrificing the wonderous convenience of the anonymous union (v.x instead of the awful v.x() I had been using).

The C-friendly alternative solution I suppose could be to wrap up the vec class into a macro that you then call for 2, 3, 4 to do the code copying, but I prefer templates for this particular problem.

Edited by poe on