4

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?

kosude
  • 177
  • 3
  • 14
  • This seems to be overthinking. No specialization is necessary. `vector, 3>, 4>, 5>` will give you the 4d vector on a silver platter. If the overloaded operators are defined correctly everything will work automatically, as it should work. – Sam Varshavchik Jan 01 '22 at 18:24
  • 2
    @SamVarshavchik I think by four-dimensional vector the asker means in the mathematical sense, which in C++ would be more like a `std::array` rather than a multiply nested vector. – Nathan Pierson Jan 01 '22 at 18:27
  • I am all for code readability, but this seems a fair bit of work indeed. I don't really have an answer but I do have some ideas : https://onlinegdb.com/rYoPRe2XW. It might help if you could show a bit of the target syntax you're looking for in a real equation/piece of code. In any case a bit of extra work is sometimes what you have to do when writing a reusable library, the end goal is to make client code more simple/robust/readable :) – Pepijn Kramer Jan 01 '22 at 18:45
  • With inheriting from lower dimension and in addition use of `constexpr if` in the largest array size you should be able to have the maximum reuse of hand crafted code parts I believe. It is not as easy to give you a perfect answer if we do not have an idea what is an example use case which differs through the dimension of your array type system. – Klaus Jan 01 '22 at 18:53

3 Answers3

10

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.

cigien
  • 57,834
  • 11
  • 73
  • 112
  • 3
    One can even SFINAE/`requires` out member functions instead of `static_assert` – yeputons Jan 01 '22 at 19:11
  • @yeputons Yeah, that's a good idea. Added it to the answer, thanks. – cigien Jan 01 '22 at 19:16
  • 2
    This also has the advantage that it means that the default-generated copy and move constructors and assignment operators will work as expected, whereas having reference member variables could cause big headaches. – Nathan Pierson Jan 01 '22 at 19:21
  • As mentioned by yeputons, constraining types with [`requires`](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rt-concepts) or even [`std::enable_if`](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rt-concept-def) is the way to go. As this will catch errors early on. – 303 Jan 01 '22 at 20:29
  • 1
    Also, as hinted by Nathan Pierson, we should really strive to adhere to value semantics as much as possible in C++. This simplifies the way we can reason about types. Ideally, a simple [concrete type](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-regular) like a vector in this case, should really be a [regular type](https://en.cppreference.com/w/cpp/concepts/regular). – 303 Jan 01 '22 at 20:29
3

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>>
{
    // ....
};
yeputons
  • 8,478
  • 34
  • 67
1

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
 }
max66
  • 65,235
  • 10
  • 71
  • 111