Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How would one efficiently reuse code in a specialised template struct?

Tags:

c++

templates

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?

like image 472
kosude Avatar asked Dec 10 '22 23:12

kosude


2 Answers

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.

like image 148
cigien Avatar answered May 13 '23 05:05

cigien


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>>
{
    // ....
};
like image 29
yeputons Avatar answered May 13 '23 05:05

yeputons