类模板特化中的成员变量别名
Posted
技术标签:
【中文标题】类模板特化中的成员变量别名【英文标题】:Member variable alias in class template specialization 【发布时间】:2019-01-03 18:47:24 【问题描述】:假设我正在编写一个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,但没有一个能满足我的所有要求。
甚至可以创建这样的别名吗?如果是的话,怎么能做到呢?
【问题讨论】:
v.x()
有什么问题?
对象必须是唯一可寻址的,成员从不共享地址。这意味着非静态数据成员,即使是无状态的数据成员,对类的大小都有有效的非零影响。所以坚持添加数据成员(Vector::x)
并坚持不增加班级规模对我来说似乎是矛盾的。 union
s 是个例外,但即使在这种情况下,在给定时间也只能有一个成员处于活动状态。它不适合使用别名。
C++ 没有变量别名。它确实有参考,但那些可以/将会带来空间成本。作为一个可能的零成本抽象可以写 int& x = my_vector.data[0];
在使用站点中创建自己的别名,并且可能会被优化掉。
@FrançoisAndrieux v.x()
没有任何问题。这个问题只是一个借口,试图解决我一直在摆弄的一个问题。
@FrançoisAndrieux 就像 NathanOliver 指出的那样,关于不增加类大小的评论与我提到的一种方法有关,即成员引用。如果我添加对data
的每个单元格的引用,有效地给它们命名,我将增加Vector
的整体大小。
【参考方案1】:
(这不是一个答案,它是一个带有代码示例的注释,它不适合作为注释,并且如果它可以塞进注释中,则格式不好。)
你能否换个方向,将向量表示为一堆字段,然后将索引 getter/setter 映射到每个字段?
取出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';
【讨论】:
N
模板参数很重要,因为它首先解决了我的问题。我已经有一个运行良好的 Vector3<T>
类,但我想分解代码并引入任意大小的向量。
嗯,但是每个命名字段都需要对每个支持的任意大小进行部分特化。哪一种违背了目的,除非它能够说Vector<int, 3>
而不是Vector3<int>
。
最大的挑战是禁用基于N
的成员。也许您可以有条件地从提供x
、y
和z
的基本类型继承?
@Eljay 目标是拥有类似using Vector3<T> = Vector<T, 3>
的东西,其中Vector<T, 3>
是Vector<T, N>
的专用版本,它将包含x
、y
和z
访问器。
@FrançoisAndrieux 这类似于标签调度,但用于成员变量。不幸的是,我没有找到任何方法将这些技术应用于变量,仅应用于成员函数。【参考方案2】:
如果允许宏,那么它似乎是可行的。
第一次尝试(不错但不完美...)
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://***.com/questions/12447557/can-we-have-recursive-macros
// leaving this as a further exercise...
http://coliru.stacked-crooked.com/a/42625e9c198e1e58
编辑:添加 operator[] 以解决评论中提出的问题。
第二次尝试:使用更好的字段名称
OP 要求字段具有更好的名称,例如 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
但是等等,operator[] 效率不高
如果 T 是标准布局类型,我想有一个更有效的 operator[],并具有以下实现:
#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
很遗憾 - 上述优化是非法的
对于呈现的实现,任何 DIMENSIONS > 1 的 Vector 不是标准布局类(即使 T 是)because it has members both in base class and in the derived class:
10.1 [class.prop]
[3] 类 S 是标准布局类,如果它: ...
[3.6] 具有类及其基类中的所有非静态数据成员和位域 在同一个类中首先声明的类...
所以上面的优化尝试具有未定义的行为 - 编译器没有义务按顺序保持继承层次结构中成员的地址。
最初的解决方案仍然有效。
【讨论】:
这样T data
数组就丢失了。 OP 希望通过别名访问它:“我希望用户能够在较小的向量的情况下访问具有有意义名称的数据,例如通过使用 v.x 或 v.y 而不是 v.data[0] 和 v.data[1] ”。否则,他们可以声明... Vector<T, 3> T x, y, z; ;
添加运算符 []【参考方案3】:
这是可能的解决方案(尽管我认为这是不好的做法,并且不确定是否可移植):
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
更新: 并让well defined 成为您可以这样做:
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]
似乎实际上不兼容布局(请参阅 here)。尽管如此,这种模式似乎被用于各种库中,用于实现简单的向量类型 - example1 (GLM)、example2 video、example3
【讨论】:
如果它不是最后一个被分配的联合成员,则从它读取是未定义的行为。这不是解决方案。 这是所有非平凡类型的 UB。 此外,即使Vector<T, 2>
(?),此解决方案也可以使z
访问。
@NathanOliver 据我了解,它是所有类型的 UB。您能否链接到解释为什么它可以用于琐碎类型的参考?
@GeorgiGerganov 这似乎是所有提供向量类型的小型数学库使用的通用模式。以上是关于类模板特化中的成员变量别名的主要内容,如果未能解决你的问题,请参考以下文章