Home > Software design >  How would one efficiently reuse code in a specialised template struct?
How would one efficiently reuse code in a specialised template struct?

Time:01-02

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
 }
  • Related