Any size math vector using Modern C++

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;