In the normal case of working with any graphics or games programming code you inevitably end up with a lot of classes for handling 2D/3D points in the world. Usually as a direct map to GLSL/HLSL's float1, float2, float3 and float4 or the int and double equivalents.
This can quickly get messy to maintain when you have tens of classes to update for any small change - not to mention tedious. A resolution to this problem is taking advantage of templates, the std::array classes and some nice new features of the language to try and (almost) make a readable single class that can be initialised to the correct type and number of elements to be used. Ideally for maximum performance we want to keep the dozens of classes to make performance analysis easier and therefore easier to make go fast, but for more general or less close to the metal applications I am presenting the class 'vecN'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | template<typename T, unsigned int elemCount> struct vecN { public: vecN() {} template<class... Args> vecN(Args... args) { const int n = sizeof...(Args); static_assert(n == elemCount, "Invalid number of arguments for vector type"); data = { { args... } }; } std::array<T, elemCount> data; } |
This is the basic backbone of the class. It is a simple vector class that can initialised like - 'vecN<float,4> someFloat4();' or 'vecN<float,4> someFloat4(1.f, 2.f, 3.f, 4.f);'.
It takes the two template parameters ('float' and '4') and uses them to initialise an std::array. The constructors offered are a simple blank constructor which will leave the array with default uninitialised values or there is a variadic template constructor. The variadic template constructor will take the number of elements in the array as an input and initialise the std::array with those values. It uses a static_assert to check at compile-time that we can only pass the correct number of elements to the array, this prevents us from passing too many or too few elements to the array.
With the class just as it is, we are able to use the vector - but a vector should provide more functionality than a simple array!
1 2 3 4 5 6 7 8 | template<class... Args> void Set(Args... args) { const int n = sizeof...(Args); static_assert(n == elemCount, "Invalid number of arguments for vector type"); data = { { args... } }; } |
We want to be able to set the values in the vector, so we can use the same code as the constructor to write a function to allow the values in the vector to be set after construction.
A good vector class should also provide basic mathematical function to make the vector easy to use with simple types and other vectors
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 | void Multiply(T _mulVal) { std::for_each(data.begin(), data.end(), [&_mulVal](T& elem) { elem*= _mulVal; }); } void Multiply(vecN<T,elemCount> _mulVal) { int counter = 0; std::for_each(data.begin(), data.end(), [&counter, &_mulVal](T& elem) { elem *= _mulVal.data[counter++]; }); } void Add(T _addVal) { std::for_each(data.begin(), data.end(), [&_addVal](T& elem) { elem += _addVal; }); } void Add(vecN<T, elemCount> _addVal) { int counter = 0; std::for_each(data.begin(), data.end(), [&counter, &_addVal](T& elem) { elem += _addVal.data[counter++]; }); } float Length() { std::array<T, elemCount> dataSqr = data; std::for_each(dataSqr.begin(), dataSqr.end(), [](T& elem) { elem = elem*elem; }); T sum = std::accumulate(dataSqr.begin(), dataSqr.end(), (T)0); return sqrt(sum); } void Normalise() { T len = 1.0/Length(); std::for_each(data.begin(), data.end(), [&len](T& elem) { elem *= len; }); } |
This code makes the class actually useful for some simple linear algebra. In this code we are using 'std::for_each' from <algorithm> and 'std::accumulate' from <numeric> to allow us to work with our arbitrary sized array generically with relative ease and confidence in the code that it will generate. C++17 allows us to apply different execution patterns on these functions, but that has not been enabled in my compiler just yet, so for now we are sticking with default behaviour, but this is an area that could be improved soon - particularly if we wanted to use a very large vector such as 'vecN<float, 2048>' !
Next we want to think of the common use cases for the classes - such as having a 'float3' and needed to pass a 'float4' which is quite common when writing to some textures or in shader pipelines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | vecN<T, elemCount - 1> PopElem() { vecN<T, elemCount - 1> output; std::copy_n(data.begin(), elemCount-1, output.data.begin()); return output; } vecN<T, elemCount + 1> PushElem(T _value = 0.0) { vecN<T, elemCount + 1> output; std::copy_n(data.begin(), elemCount, output.data.begin()); output.data[elemCount] = _value; return output; } template<int _size> constexpr vecN<T, _size> GetResizedVector() { vecN<T, _size> output; std::copy_n(data.begin(), std::min((unsigned int)(_size), elemCount), output.data.begin()); return output; } |
Using the push and pop functions provided here we can arbitrarily return a copy of the vector with one more or less element. Or, we can use 'GetResizedVector' to request an arbitrary sized copy when we need to do larger adjustments. Nothing too fancy going on with these functions. We are using 'std::copy_n' to copy a sub-range of the array when shrinking, in the past this might have been done with memcpy, but this is a lot safer.
Next we want to look at accessing data from the vector in a nice way.
1 2 3 4 5 6 7 8 9 10 | constexpr T& GetElement(int _index) { return data[_index]; } template<int _index> constexpr T& GetElement() { return data[_index]; } |
These two little functions give the user two ways to access the data. The second function is done using the template in the hope that the compiler will recognise the constant pointer offset and save on evaluating '[_index]' at run-time where possible - just like how a normal 'someFloat3.x' would behave. They are also constexpr where possible to allow for aggressive compilers to resolve values where ever possible.
This isn't sufficient for all use cases though we also want to provide standard x/y/z/w/xyz access.
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 | constexpr T& x() { static_assert(1 <= elemCount, "Invalid number of arguments for vector type"); return data[0]; } constexpr T& y() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); return data[1]; } constexpr T& z() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); return data[2]; } constexpr T& w() { static_assert(4 <= elemCount, "Invalid number of arguments for vector type"); return data[3]; } constexpr std::array<T, 3> xxx() { static_assert(1 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[0], data[0], data[0] }; return xyz; } ... and many more ... |
These functions, and many more not shown provide for all the element access methods that a user will be used to when dealing with 'float1-4' types. Where possible we are statically asserting to prevent out of range data access, so 'someFloat3.z()' will only work on arrays that have at least 3 elements.
But what if our user has a 6 element array and wants the first and last elements only...?
1 2 3 4 5 6 7 | template<class... Indexs> constexpr vecN<T, sizeof...(Indexs)> GetOrderedArrayOfIndices(Indexs... indxs) { vecN<T, sizeof...(Indexs)> output; output.data = { { data[indxs]... } }; return output; } |
In that case, we have to use variadics again and similarly to how we initialised the array of the class with the arguments in the constructor and 'Set' function we need to expand the variadic input, but this time, expanding with the pattern of 'data[indxs]...' so that instead of getting:
{ { value1, value2, value3 and on and on } }
we get:
{ {data[value1], data[value2], data[value3] and on and on } }
This allows us to call the function like 'someVector4.GetOrderedArrayOfIndicies(0,3);' to get a vector with two elements which hold the first and last element values of 'someVector4'. With this method we can arbitrarily construct arrays from other array or perform reordering.
Finally, we want to be able to work with these types without excessive typing of hard to read code, so we can hide most of the templating in the types with a few simple typedefs...
1 2 3 4 | typedef vecN<float, 4> float4; typedef vecN<float, 3> float3; typedef vecN<float, 2> float2; typedef vecN<float, 1> float1; |
So, now the user can simple create a 'float4' and use it like any other class they are used to- but easily convert sizes and access elements however they want.
Thanks for reading!
Full Code:
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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | template<typename T, unsigned int elemCount> struct vecN { public: vecN() {} template<class... Args> vecN(Args... args) { Set(args...); } template<class... Args> void Set(Args... args) { const int n = sizeof...(Args); static_assert(n == elemCount, "Invalid number of arguments for vector type"); data = { { args... } }; } void Multiply(T _mulVal) { std::for_each(data.begin(), data.end(), [&_mulVal](T& elem) { elem*= _mulVal; }); } void Multiply(vecN<T,elemCount> _mulVal) { int counter = 0; std::for_each(data.begin(), data.end(), [&counter, &_mulVal](T& elem) { elem *= _mulVal.data[counter++]; }); } void Add(T _addVal) { std::for_each(data.begin(), data.end(), [&_addVal](T& elem) { elem += _addVal; }); } void Add(vecN<T, elemCount> _addVal) { int counter = 0; std::for_each(data.begin(), data.end(), [&counter, &_addVal](T& elem) { elem += _addVal.data[counter++]; }); } float Length() { std::array<T, elemCount> dataSqr = data; std::for_each(dataSqr.begin(), dataSqr.end(), [](T& elem) { elem = elem*elem; }); T sum = std::accumulate(dataSqr.begin(), dataSqr.end(), (T)0); return sqrt(sum); } void Normalise() { T len = 1.0/Length(); std::for_each(data.begin(), data.end(), [&len](T& elem) { elem *= len; }); } vecN<T, elemCount - 1> PopElem() { vecN<T, elemCount - 1> output; std::copy_n(data.begin(), elemCount-1, output.data.begin()); return output; } vecN<T, elemCount + 1> PushElem(T _value = 0.0) { vecN<T, elemCount + 1> output; std::copy_n(data.begin(), elemCount, output.data.begin()); output.data[elemCount] = _value; return output; } template<int _size> constexpr vecN<T, _size> GetResizedVector() { vecN<T, _size> output; std::copy_n(data.begin(), std::min((unsigned int)(_size), elemCount), output.data.begin()); return output; } constexpr T& GetElement(int _index) { return data[_index]; } template<int _index> constexpr T& GetElement() { return data[_index]; } template<class... Indexs> constexpr vecN<T, sizeof...(Indexs)> GetOrderedArrayOfIndices(Indexs... indxs) { vecN<T, sizeof...(Indexs)> output; output.data = { { data[indxs]... } }; return output; } constexpr T& x() { static_assert(1 <= elemCount, "Invalid number of arguments for vector type"); return data[0]; } constexpr T& y() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); return data[1]; } constexpr T& z() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); return data[2]; } constexpr T& w() { static_assert(4 <= elemCount, "Invalid number of arguments for vector type"); return data[3]; } constexpr std::array<T, 3> xxx() { static_assert(1 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[0], data[0], data[0] }; return xyz; } constexpr std::array<T, 3> yyy() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[1], data[1], data[1] }; return xyz; } constexpr std::array<T, 3> zzz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[2], data[2], data[2] }; return xyz; } constexpr std::array<T, 3> xyz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[0], data[1], data[2] }; return xyz; } constexpr std::array<T, 3> xzy() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[0], data[2], data[1] }; return xyz; } constexpr std::array<T, 3> yzx() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[1], data[2], data[0] }; return xyz; } constexpr std::array<T, 3> yxz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[1], data[0], data[2] }; return xyz; } constexpr std::array<T, 3> zyx() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[2], data[1], data[0] }; return xyz; } constexpr std::array<T, 3> zxy() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 3> xyz = { data[2], data[0], data[1] }; return xyz; } constexpr std::array<T, 2> xx() { static_assert(1 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[0], data[0] }; return xyz; } constexpr std::array<T, 2> xy() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[0], data[1] }; return xyz; } constexpr std::array<T, 2> xz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[0], data[2] }; return xyz; } constexpr std::array<T, 2> yx() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[1], data[0] }; return xyz; } constexpr std::array<T, 2> yy() { static_assert(2 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[1], data[1] }; return xyz; } constexpr std::array<T, 2> yz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[1], data[2] }; return xyz; } constexpr std::array<T, 2> zx() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[2], data[0] }; return xyz; } constexpr std::array<T, 2> zy() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[2], data[1] }; return xyz; } constexpr std::array<T, 2> zz() { static_assert(3 <= elemCount, "Invalid number of arguments for vector type"); std::array<T, 2> xyz = { data[2], data[1] }; return xyz; } std::array<T, elemCount> data; }; typedef vecN<float, 4> float4; typedef vecN<float, 3> float3; typedef vecN<float, 2> float2; typedef vecN<float, 1> float1; |