Reputation: 786
Let's suppose I'm writing a Vector
template class to represent points and vectors in an N-dimensional space. Something like the following:
template <typename T, int N>
struct Vector
{
T data[N];
// ...
};
Let's further assume that, for whatever reason, I want the user to be able to access data
with meaningful names in the case of smaller vectors, e.g. by using v.x
or v.y
instead of v.data[0]
and v.data[1]
.
I have two additional constraints.
x
or y
component of a vector should not be written as a function call (e.g. it should be v.x
, not v.x()
).sizeof(Vector<T, N>) == N * sizeof(T)
.I looked at different possible approaches, including member variable references, tag dispatch and even CRTP but none of them satisfied all of my requirements.
Is it even possible to create such aliases? And if yes, how could one do it?
Upvotes: 4
Views: 973
Reputation: 13810
If macros are allowed then it seems doable.
int main() {
Vector<int, 4> vec;
vec[0] = 1; // same as: vec.t1 = 1;
vec[1] = 2; // same as: vec.t2 = 2;
vec[2] = 3; // same as: vec.t3 = 3;
vec[3] = 4; // same as: vec.t4 = 4;
std::cout << vec.t1 + vec.t2 + vec.t3 + vec.t4; // 10
}
To achieve the above:
#define VAR_NAME(num) t##num
#define DefineVector(num) \
template<typename T> \
struct Vector<T, num> : Vector<T, num-1> { \
T VAR_NAME(num); \
T& operator[](int index) { \
if(index == num-1) return VAR_NAME(num); \
return Vector<T, num-1>::operator[](index); \
} \
}
template<typename T, size_t N>
struct Vector;
template<typename T>
struct Vector<T, 1> {
T t1;
T& operator[](int index) {
// in case index != 0 this is UB
return t1;
}
};
DefineVector(2);
DefineVector(3);
DefineVector(4);
// TODO:
// replace 3 declarations above with a single *DefineVectorsRecursively(4);*
// by using recursive macros
// see: https://stackoverflow.com/questions/12447557/can-we-have-recursive-macros
// leaving this as a further exercise...
http://coliru.stacked-crooked.com/a/42625e9c198e1e58
EDIT: Added operator[] to address the concern raised in comment.
The OP requested the fields to have nicer names, like x, y, z.
This is a challenge. But macros come again to the rescue:
int main() {
Vector<int, 3> vec;
vec[0] = 1;
vec[1] = 2;
vec[2] = 3;
std::cout << vec.x + vec.y + vec.z; // 6
}
With the following code:
#include <boost/preprocessor/variadic/size.hpp>
template<typename T, size_t DIMENSIONS>
struct Vector;
#define DefineVector(VAR, ...) \
template<typename T> \
struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
: Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)> { \
T VAR; \
T& operator[](int index) { \
if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
} \
}
#define DefineVector1(VAR) \
template<typename T> \
struct Vector<T, 1> { \
T VAR; \
T& operator[](int index) { \
/* in case index != 0 this is UB */ \
return VAR; \
} \
}
DefineVector1(x);
DefineVector(y, x);
DefineVector(z, y, x);
// TODO: create recursive macro for DefineVector(z, y, x)
// that will create the two above recursively
Code: http://coliru.stacked-crooked.com/a/2550eede71dc9b5e
I had a thought of having a more efficient operator[] in case T is a standard layout type, with the following implementation:
#define DefineVector(VAR, ...) \
template<typename T> \
struct Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) + 1> \
: Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)> { \
T VAR; \
T& operator[](int index) { \
if constexpr(std::is_standard_layout_v<T>) { \
return *(&VAR - (BOOST_PP_VARIADIC_SIZE(__VA_ARGS__) - index)); \
} else { \
if(index == BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)) return VAR; \
return Vector<T, BOOST_PP_VARIADIC_SIZE(__VA_ARGS__)>::operator[](index); \
} \
} \
}
The (BAD) attempt: http://coliru.stacked-crooked.com/a/d367e770f107995f
For the presented implementation, any Vector with DIMENSIONS > 1 is not a standard layout class (even if T is) because it has members both in base class and in the derived class:
10.1 [class.prop]
[3] A class S is a standard-layout class if it: ...
[3.6] has all non-static data members and bit-fields in the class and its base classes first declared in the same class ...
So the optimization attempt above has undefined behavior - compiler is not obliged to keep the address of the members in the inheritance hierarchy in their order.
The initial solution is still valid.
Upvotes: 0
Reputation: 1066
Here is possible solution (although I think this is bad practice, and not really sure if portable):
template <typename T, int N>
union Vector
{
struct { T x, y, z; };
T data[N];
};
Here is example what happens:
int main() {
Vector<int, 10> vec;
vec.x = 100;
vec.y = 200;
vec.z = 300;
vec.data[3] = vec.data[2] + 100;
printf("%d %d %d %d\n", vec.data[0], vec.data[1], vec.data[2], vec.data[3]);
printf("size = %d\n", (int) sizeof(vec));
return 0;
}
Output:
100 200 300 400
size = 40
Update: and to make this well defined you can do:
template <typename T, int N> union Vector;
template <typename T> union Vector<T, 1> {
struct { T x; };
T data[1];
};
template <typename T> union Vector<T, 2> {
struct { T x, y; };
T data[2];
};
template <typename T> union Vector<T, 3> {
struct { T x, y, z; };
T data[3];
};
template <typename T> union Vector<T, 4> {
struct { T x, y, z, w; };
T data[4];
};
Just make sure the struct
is standard-layout (i.e. this works for T = int, float, double, etc.).
Update 2: note that the above might still be UB, because T x, y, z
and T data[3]
seem to not actually be layout-compatible (see here). Still, this pattern seems to be used in various libraries, for implementing simple vector types - example1 (GLM), example2 video, example3
Upvotes: -1
Reputation: 5321
(This is not an answer, it is a comment with a code example, which doesn't fit as a comment, and doesn't format well if it could be stuffed into a comment.)
Can you go the other direction, and express the vector as a bunch of fields, then map an index getter/setter to each of those fields?
Taking out the N template parameter to simplify the problem:
#include <iostream>
#include <stdexcept>
template <typename T>
struct Vector3
{
T x;
T y;
T z;
T operator[](int i) const
{
switch(i)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw std::out_of_range("out of range");
}
}
T& operator[](int i)
{
switch(i)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw std::out_of_range("out of range");
}
}
};
int main()
{
Vector3<float> v;
v.x = 1.0f;
v[1] = 2.0f;
v.z = 3.0f;
std::cout << v[0] << " " << v.y << " " << v[2] << '\n';
}
Upvotes: 2