3

I am creating a Vector class and instead of creating separate classes for Vector2, Vector3 and Vector4 I want to make an all-in-one class. My problem is that I want to define certain properties depending on the size of the vector. For example, a vector of size 2 will have an x and y component, but not a z component.

I'm attempting to use the #if directive to check the non-type template argument (SIZE) to create properties depending on the size of the vector. I did some research and I don't think it's possible this way. I'm wondering if anyone has an idea about how to achieve want I want?

template <typename T, int SIZE>
class Vector
{
public:
    union
    {
        struct
        {
#if SIZE == 2
            T x, y;
#endif
#if SIZE == 3
            T z;
#endif
#if SIZE == 4
            T w;
#endif
        };
        T data[SIZE];
    };
};

I want to be able to create and access a Vector like so:

Vector<int, 2> vec2;
vec2.x;
Vector<int, 3> vec3;
vec3.z;

Any feedback is appreciated!

EDIT: After reviewing the comments I came up with a nifty solution... Hopefully it works for me. I created a templated class:

template <typename T, unsigned int SIZE> class _vec;

This deals with the data (components) of the vector and contains the behaviour. Then I made another templated class that will sort of specialise _vec like so:

template <typename T, unsigned int SIZE> class Vector : public _vec<T, SIZE>;

This is a vector of any size. Now I can specialise that for Vector2, Vector3 and Vector4:

template <typename T> class Vector<T, 2> : public _vec<T, 2>;

template <typename T>
using Vector2 = Vector<T, 2>;

I'll let you know how HORRIBLE this will turn out... ;P

EDIT 2: It worked, but didn't work out... :(

  • 1
    You can specialize your template? Although to be honest this doesn't look like good design. – freakish May 15 '19 at 07:36
  • Is there a particular reason for this? Most if not all of the vector math libraries i've ever seen just have seperate `Vec2` `Vec3` and `Vec4` implementations. Tbh even templating the components (from what I can see at least) seems like overkill. I wouldn't want to constantly have to choose between different types or needlessly state the same type each time and end up paying conversion/cast costs. – George May 15 '19 at 07:41
  • I agree with @freakish here. This code is not going to expand very well! Remember that later on you will have to write operator overloads and other functions which you must use thos macros to differentiate. Go with specialization, or simply create one vector class which defaults `z`, and/or `w`, to `0`. – Constantinos Glynos May 15 '19 at 07:41
  • Preprocessing is a separate phase, before compilation. The preprocessor has no idea about what C++ identifiers could mean. – molbdnilo May 15 '19 at 07:43
  • 2
    Btw, I know that union-structs are used a lot, but keep in mind that they are considered undefined behaviour (UB). More on this question [here](https://stackoverflow.com/q/55462197/2754510). – Constantinos Glynos May 15 '19 at 07:49
  • giving the members different names is somehow against the spirit of using templates. Not only are `Vector` and `Vector` different types, but also you cannot really write generic code that treats them equally. One quacks like a duck, the other barks like a dog... – 463035818_is_not_an_ai May 15 '19 at 07:55
  • @formerlyknownas_463035818: OP's code is clearly done to allow `Vec<2, int> v; v[0] = 42; foo(v.x);`, which is UB. – Jarod42 May 15 '19 at 08:21
  • @George yeah, I understand that - it's for an assignment that requires templated components. – Davinatoratoe May 15 '19 at 08:41
  • @ConstantinosGlynos I had that idea too (one class that defaults z and/or w to 0). But this would not work inside the union, right? I'd have to separate the components out as pointers or something. – Davinatoratoe May 15 '19 at 08:48
  • @Jarod42 hm right, sorry – 463035818_is_not_an_ai May 15 '19 at 10:04
  • @Davinatoratoe: It should work just fine. The only downside is that you would be performing unnecessary computations for `z` and `w`, since they'd be defaulted to `0`. Other than, I don't see why it would fail. – Constantinos Glynos May 15 '19 at 10:09

3 Answers3

4

No, this is not possible.

In the phases of translation, the preprocessing phase comes before the template instantiation phase. So in this case, you will end up having none of the variables defined in the template Vector because SIZE is not defined at the preprocessing phase.

As per the C++ standard, [cpp.cond]/9:

After all replacements due to macro expansion and evaluations of defined-macro-expressions and has-include-expressions have been performed, all remaining identifiers and keywords, except for true and false, are replaced with the pp-number 0, and then each preprocessing token is converted into a token.

So the value of SIZE here will be 0 and thus none of the conditions of the conditional inclusions are met.

You can instead specialize your template to have different variables for different instantiations.

P.W
  • 26,289
  • 6
  • 39
  • 76
1

Disclaimer: I got that idea from this codebase. It seems a few users have pointed this out in the comments as well.


No. That's not possible with pre-processor directives, but you can use template specialization instead:

template <typename T, unsigned int S>
struct vec {
  T data[S];
};

template <typename T>
struct vec<T, 2> {
  T x, y;
};

template <typename T>
struct vec<T, 3> {
  T x, y, z;
};

template <typename T>
struct vec<T, 4> {
  T x, y, z, w;
};

If you really want to, you can add the union back in but that's undefined behavior. You can instead make x, y and z functions and have them return a reference into the array:

template <typename T, unsigned int S>
struct vec {
  T data[S];
};

template <typename T>
struct vec<T, 2> {
  T data[2];

  T& x() { return data[0]; }
  const T& x() const { return data[0]; }

  T& y() { return data[1]; }
  const T& y() const { return data[1]; }
};

template <typename T>
struct vec<T, 3> {
  T data[3];

  T& x() { return data[0]; }
  const T& x() const { return data[0]; }

  T& y() { return data[1]; }
  const T& y() const { return data[1]; }

  T& z() { return data[2]; }
  const T& z() const { return data[2]; }
};

template <typename T>
struct vec<T, 4> {
  T data[4];

  T& x() { return data[0]; }
  const T& x() const { return data[0]; }

  T& y() { return data[1]; }
  const T& y() const { return data[1]; }

  T& z() { return data[2]; }
  const T& z() const { return data[2]; }

  T& w() { return data[3]; }
  const T& w() const { return data[3]; }
};

If you have stuff that should be available in all classes, you can introduce another class e.g. vec_ which does that and inherit from it (add the data array there and have e.g. scalar product only access the array).

If you are using C++17 I would recommend adding [[nodiscard]] constexpr to the x, y and z functions but that would just clutter up the example.

asynts
  • 2,213
  • 2
  • 21
  • 35
  • Very helpful! The vector will have behaviour (normalisation, operator overloading, etc.) and the components cannot be accessed via a function call (this is for an assignment). However your explanation of specialised templates is very interesting and helpful. Thank you! – Davinatoratoe May 15 '19 at 08:45
0

As said in the comments, this design is perhaps not the best, and cannot work with macros.

But if you want to give it a try, you could already simplify the job using array<T, size> data and creating functions for natural names:

inline T x() const { return data[0]; }

and so on. Now things become really tricky, because for instance the z() function should exist only for vector of size 3 and 4. Using template black magic and SFINAE should make it possible, but I don't know if the effort worth it. See Vittorio Romeo site for complete explanation.

gaFF
  • 747
  • 3
  • 11