I am creating my own vector struct for a maths library.
Currently, I would create the struct somewhat like this:
template <unsigned int size, typename T>
struct vector {
// array of elements
T elements[size];
// ...
};
However, the main use case of the maths library will lead to mostly making use of 2-dimensional, 3-dimensional, and 4-dimensional vectors (commonly vec2
, vec3
, and vec4
). Because of this, a useful feature would be the ability to access the x, y, z, and w values from the vector when possible. However, there are some problems with this.
The x
, y
, z
, and w
members would need to be reference variables to elements[0]
, elements[1]
, etc. This means that, if the vector has less than 4 elements, some references would not be initialised.
Of course, this is possible to achieve with specialised templates, and this is what I am currently doing:
template <unsigned int size, typename T>
struct vector {
// ...
}
template <typename T>
struct vector<2, T> {
// same as with before, except with references to X and Y elements.
// these are successfully initialised in the constructor because the vector is guaranteed to have 2 elements
T &x;
T &y;
// ...
}
// and so on for 3D and 4D vectors
This works, but it is far from convenient. In practice, the vector
struct is large and has a lot of functions and operator overloads. When it is specialised into the other sizes, these functions and operator overloads need to be copy pasted from the generic struct to 2D, 3D and 4D structs, which is very inefficient. Keep in mind: the only thing I'm changing between specialisations is the reference variables! All other members are the exact same, and so I'd rather reuse their code.
One other solution is to inherit from one base class. I'm not entirely sure how to do this in a way that allows the inherited operator overloads to return the values from the child vector structs rather than the values from the parent struct.
So, my question is: how would I efficiently reuse the code in a specialised template struct whilst still being able to have (in this case) the x
, y
, z
, and w
references, when available?
CodePudding user response:
If you're willing to change the interface slightly by accessing the members via a function, i.e.
vector<2, int> v;
v.x() = 5; // instead of v.x = 5;
then you can do this without any specializations at all, and sidestep the issue of code reuse entirely.
In the class template, just add as many member functions for each index you could possibly want, and assert that the access is valid:
template <unsigned int size, typename T>
struct vector {
T elements[size];
// ...
T& x() {
static_assert(size > 0);
return elements[0];
}
T& y() {
static_assert(size > 1);
return elements[1];
}
// ... and so on
};
Now this will work when accessing appropriate elements, and give an error otherwise.
vector<1, int> v1;
vector<2, int> v2;
v1.x() = 5; // ok
v1.y() = 4; // error, v1 can only access x
v2.y() = 3; // ok, v2 is big enough
Here's a demo.
Instead of the static_assert
, you can write a requires
constraint for the member functions
T& x() requires (size > 0) {
return elements[0];
}
// etc ...
Here's a demo.
CodePudding user response:
As correctly noted in comments for another answer, having reference fields is a big pain because you cannot reassign references, hence operator=
is not generated automatically. Moreover, you cannot really implement it yourself. Also, on a typical implementation a reference field still occupies some memory even if it points inside the structure.
However, for completeness, here is my answer: in C metaprogramming, if you need to dynamically add/remove fields into a class, you can use inheritance. You may also use Curiously Recurring Template Pattern (CRTP) to access the derived struct from the base.
One possible implementation is below. vector_member_aliases<size, T, Derived>
is a base for a class Derived
which provides exactly min(0, size)
member references with names from x
, y
, z
, w
. I also use inheritance between them to avoid code duplication.
#include <iostream>
template <unsigned int size, typename T, typename Derived>
struct vector_member_aliases : vector_member_aliases<3, T, Derived> {
T &w = static_cast<Derived*>(this)->elements[3];
};
template <typename T, typename Derived>
struct vector_member_aliases<0, T, Derived> {};
template <typename T, typename Derived>
struct vector_member_aliases<1, T, Derived> : vector_member_aliases<0, T, Derived> {
T &x = static_cast<Derived*>(this)->elements[0];
};
template <typename T, typename Derived>
struct vector_member_aliases<2, T, Derived> : vector_member_aliases<1, T, Derived> {
T &y = static_cast<Derived*>(this)->elements[1];
};
template <typename T, typename Derived>
struct vector_member_aliases<3, T, Derived> : vector_member_aliases<2, T, Derived> {
T &z = static_cast<Derived*>(this)->elements[2];
};
template <unsigned int size, typename T>
struct vector : vector_member_aliases<size, T, vector<size, T>> {
// array of elements
T elements[size]{};
void print_all() {
for (unsigned int i = 0; i < size; i ) {
if (i > 0) {
std::cout << " ";
}
std::cout << elements[i];
}
std::cout << "\n";
}
};
int main() {
[[maybe_unused]] vector<0, int> v0;
// v0.x = 10;
vector<1, int> v1;
v1.x = 10;
// v1.y = 20;
v1.print_all();
vector<2, int> v2;
v2.x = 11;
v2.y = 21;
// v2.z = 31;
v2.print_all();
vector<3, int> v3;
v3.x = 12;
v3.y = 22;
v3.z = 32;
// v3.w = 42;
v3.print_all();
vector<4, int> v4;
v4.x = 13;
v4.y = 23;
v4.z = 33;
v4.w = 43;
v4.print_all();
std::cout << sizeof(v4) << "\n";
}
Another implementation is to create four independent classes and use std::condition_t
to choose from which to inherit, and which to replace with some empty_base
(distinct for each skipped variable):
#include <iostream>
#include <type_traits>
template<int>
struct empty_base {};
template <typename T, typename Derived>
struct vector_member_alias_x {
T &x = static_cast<Derived*>(this)->elements[0];
};
// Skipped: same struct for for y, z, w
template <unsigned int size, typename T>
struct vector
: std::conditional_t<size >= 1, vector_member_alias_x<T, vector<size, T>>, empty_base<0>>
, std::conditional_t<size >= 2, vector_member_alias_y<T, vector<size, T>>, empty_base<1>>
, std::conditional_t<size >= 3, vector_member_alias_z<T, vector<size, T>>, empty_base<2>>
, std::conditional_t<size >= 4, vector_member_alias_w<T, vector<size, T>>, empty_base<3>>
{
// ....
};
CodePudding user response:
Not sure to understand what do you exactly want, but... just for fun... you can write a self recursive base class as follows
// generic case: starting recursion point for size > 3
template <std::size_t size, typename T, std::size_t sizeArr>
struct myVectorBase : public myVectorBase<3u, T, sizeArr>
{ T & w = myVectorBase<3u, T, sizeArr>::elements[3u]; };
// recursion ground case: elements definition
template <typename T, std::size_t sizeArr>
struct myVectorBase<0u, T, sizeArr>
{ T elements[sizeArr]; };
// special case for myVector<0, T>: an array of size zero isn't standard
template <typename T>
struct myVectorBase<0u, T, 0u>
{ };
template <typename T, std::size_t sizeArr>
struct myVectorBase<1u, T, sizeArr> : public myVectorBase<0u, T, sizeArr>
{ T & x = myVectorBase<0u, T, sizeArr>::elements[0u]; };
template <typename T, std::size_t sizeArr>
struct myVectorBase<2u, T, sizeArr> : public myVectorBase<1u, T, sizeArr>
{ T & y = myVectorBase<1u, T, sizeArr>::elements[1u]; };
template <typename T, std::size_t sizeArr>
struct myVectorBase<3u, T, sizeArr> : public myVectorBase<2u, T, sizeArr>
{ T & z = myVectorBase<2u, T, sizeArr>::elements[2u]; };
that define a elements[sizeArr]
array (inherited from myVectorBase<0u, T, sizeArr>
), an element x
(inherited from myVectorBase<1u, T, sizeArr>
, when the starting size
is greater than zero), an element y
(inherited from myVectorBase<2u, T, sizeArr>
, when the starting size
is greater than 1), an element z
(inherited from myVectorBase<3u, T, sizeArr>
, when the starting size
is greater than 2) and an alement w
(inherited from myVectorBase<4u, T, sizeArr>
, when the starting size
is greater than 3)
Now you can define your myVector
(please, avoid the use of names as vector
that are used in the standard library) as follows
template <std::size_t size, typename T>
struct myVector : myVectorBase<size, T, size>
{ };
The following is a full compiling example that shows that w
is available in myVector<5u, int>
but not in myVector<2u, int>
(where y
is available)
#include <iostream>
template <std::size_t size, typename T, std::size_t sizeArr>
struct myVectorBase : public myVectorBase<3u, T, sizeArr>
{ T & w = myVectorBase<3u, T, sizeArr>::elements[3u]; };
template <typename T, std::size_t sizeArr>
struct myVectorBase<0u, T, sizeArr>
{ T elements[sizeArr]; };
template <typename T>
struct myVectorBase<0u, T, 0u>
{ };
template <typename T, std::size_t sizeArr>
struct myVectorBase<1u, T, sizeArr> : public myVectorBase<0u, T, sizeArr>
{ T & x = myVectorBase<0u, T, sizeArr>::elements[0u]; };
template <typename T, std::size_t sizeArr>
struct myVectorBase<2u, T, sizeArr> : public myVectorBase<1u, T, sizeArr>
{ T & y = myVectorBase<1u, T, sizeArr>::elements[1u]; };
template <typename T, std::size_t sizeArr>
struct myVectorBase<3u, T, sizeArr> : public myVectorBase<2u, T, sizeArr>
{ T & z = myVectorBase<2u, T, sizeArr>::elements[2u]; };
template <std::size_t size, typename T>
struct myVector : myVectorBase<size, T, size>
{ };
int main ()
{
myVector<5u, int> m5i;
m5i.w = 42; // size is 5 -> w is available
std::cout << m5i.elements[3u] << '\n'; // print 42
myVector<2u, int> m2i;
// m2i.w = 37; // compilation error: size is 2 -> w is unavailable
// m2i.z = 37; // compilation error: size is 2 -> z is unavailable
m2i.y = 37; // size is 2 -> y is unavailable
std::cout << m2i.elements[1u] << '\n'; // print 37
myVector<0u, int> m0i; // compile but is empty
}