Псевдоним переменной-члена в специализации шаблона класса
Давайте предположим, что я пишу Vector
Шаблонный класс для представления точек и векторов в N-мерном пространстве. Что-то вроде следующего:
template <typename T, int N>
struct Vector
{
T data[N];
// ...
};
Далее давайте предположим, что по какой-либо причине я хочу, чтобы пользователь мог получить доступ к data
со значимыми именами в случае меньших векторов, например, с помощью v.x
или же v.y
вместо v.data[0]
а также v.data[1]
,
У меня есть два дополнительных ограничения.
- Доступ к
x
или жеy
компонент вектора не должен быть записан как вызов функции (например, он должен бытьv.x
неv.x()
). - Следующее равенство должно выполняться
sizeof(Vector<T, N>) == N * sizeof(T)
,
Я рассмотрел различные возможные подходы, включая ссылки на переменные-члены, отправку тегов и даже CRTP, но ни один из них не удовлетворил все мои требования.
Можно ли даже создать такие псевдонимы? И если да, как можно это сделать?
3 ответа
(Это не ответ, это комментарий с примером кода, который не подходит как комментарий и плохо форматируется, если его можно вставить в комментарий.)
Можете ли вы пойти в другом направлении, и выразить вектор в виде набора полей, а затем сопоставить получатель / установщик индекса с каждым из этих полей?
Извлечение параметра N шаблона для упрощения задачи:
#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';
}
Если макросы разрешены, то это кажется выполнимым.
Первая попытка (хорошо, но не идеально...)
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
}
Для достижения вышеуказанного:
#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://stackru.com/questions/12447557/can-we-have-recursive-macros
// leaving this as a further exercise...
http://coliru.stacked-crooked.com/a/42625e9c198e1e58
РЕДАКТИРОВАТЬ: Добавлен оператор [] для решения проблемы, поднятой в комментарии.
Вторая попытка: с более хорошими именами полей
ОП запросил, чтобы у полей были более хорошие имена, такие как x, y, z.
Это вызов. Но макросы снова приходят на помощь:
int main() {
Vector<int, 3> vec;
vec[0] = 1;
vec[1] = 2;
vec[2] = 3;
std::cout << vec.x + vec.y + vec.z; // 6
}
Со следующим кодом:
#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
Код: http://coliru.stacked-crooked.com/a/2550eede71dc9b5e
Но подождите, оператор [] не так эффективен
У меня была мысль о более эффективном операторе [] в случае, если T является стандартным типом макета со следующей реализацией:
#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); \
} \
} \
}
Попытка (ПЛОХО): http://coliru.stacked-crooked.com/a/d367e770f107995f
К сожалению - вышеуказанная оптимизация незаконна
Для представленной реализации любой Vector с DIMENSIONS > 1 не является стандартным классом макета (даже если T), поскольку он имеет члены как в базовом классе, так и в производном классе:
10.1 [class.prop]
[3] Класс S является классом стандартной компоновки, если он:...
[3.6] содержит все нестатические члены-данные и битовые поля в классе, а его базовые классы впервые объявлены в одном и том же классе...
Таким образом, приведенная выше попытка оптимизации имеет неопределенное поведение - компилятор не обязан сохранять адреса членов в иерархии наследования в их порядке.
Первоначальное решение остается в силе.
Вот возможное решение (хотя я думаю, что это плохая практика, и я не совсем уверен, переносимо ли это):
template <typename T, int N>
union Vector
{
struct { T x, y, z; };
T data[N];
};
Вот пример того, что происходит:
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
Обновление: и чтобы сделать это хорошо определенным, вы можете сделать:
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];
};
Просто убедитесь, что struct
является стандартным макетом (то есть это работает для T = int, float, double и т. д.).
Обновление 2: обратите внимание, что выше все еще может быть UB, потому что T x, y, z
а также T data[3]
кажется, на самом деле не совместимы с макетом (см. здесь). Тем не менее, этот шаблон, кажется, используется в различных библиотеках для реализации простых векторных типов - example1 (GLM), example2 video, example3