方便的矢量3f类

convenient Vector3f class

本文关键字:3f 方便      更新时间:2023-10-16

有时需要一个Vector3f类,它有xyz成员,并且可以同时索引为float[3]数组(SO已经有几个关于这个问题的问题)。

像这样:

struct Vector3f {
float data[3];
float &x = data[0];
float &y = data[1];
float &z = data[2];
};

有了这个,我们可以这样写:

Vector3f v;
v.x = 2.0f;
v.y = 3.0f;
v.z = 4.0f;
glVertex3fv(v.data);

但是这种实现很糟糕,因为引用占用了struct的空间(这是非常不幸的。在这种特殊情况下,我看不出有什么理由不能删除引用,也许是编译器部分错过了优化)。

但是,有了[[no_unique_address]]我就有了这个想法:

#include <new>
template <int INDEX>
class Vector3fProperty {
public:
operator float() const {
return propertyValue();
}
float &operator=(float value) {
float &v = propertyValue();
v = value;
return v;
}
private:
float &propertyValue() {
return std::launder(reinterpret_cast<float*>(this))[INDEX];
}
float propertyValue() const {
return std::launder(reinterpret_cast<const float*>(this))[INDEX];
}
};
struct Vector3f {
[[no_unique_address]]
Vector3fProperty<0> x;
[[no_unique_address]]
Vector3fProperty<1> y;
[[no_unique_address]]
Vector3fProperty<2> z;
float data[3];
};
static_assert(sizeof(Vector3f)==12);

所以,基本上,我在struct中有属性,它处理对xyz的访问。这些属性不应占用空间,因为它们是空的,并且具有[[no_unique_address]]

您如何看待这种方法?它有UB吗?


请注意,这个问题是关于一个类的,对于它,所有这些都是可能的:

Vector3f v;
v.x = 1;
float tmp = v.x;
float *c = v.<something>; // there, c points to a float[3] array

如果这将存在于标头中,并且您对编译器的优化功能有一定的信心,那么您可能会坚持使用普通的旧operator[]()重载,并期望编译器足够聪明以省略调用并返回所需的元素。 例如:

class Vec3f {
public:
float x;
float y;
float z;
float &operator[](int i) {
if(i == 0) {
return x;
}
if(i == 1) {
return y;
}
if(i == 2) {
return z;
}
}
};

我把它扔到编译器资源管理器(https://godbolt.org/z/0X4FPL)中,它显示了clang在-O2和GCC在-O3优化operator[]调用。不如你的方法那么令人兴奋,但很简单,在大多数情况下应该有效。

但是这种实现很糟糕,因为引用在结构中占用了空间(这是非常不幸的。在这种特殊情况下,我看不出有什么理由不能删除引用,也许是编译器部分错过了优化)。

这看起来是一个复杂的问题。标准布局类必须彼此兼容。因此,编译器不允许消除任何成员,无论它们是如何定义的。对于非标准布局?谁知道呢。有关更多信息,请阅读以下内容:C++标准是否保证未使用的私有字段会影响大小?

根据我的经验,编译器从不删除类成员,即使它们是"未使用的"(例如,正式sizeof确实使用它们)。

它有UB吗?

我认为这是UB。首先[[no_unique_address]]仅意味着会员不需要有唯一的地址,而不是说它不能有一个唯一的地址。其次,不清楚您的data成员从哪里开始。同样,编译器可以自由使用或不使用以前[[no_unique_address]]类成员的填充。这意味着您的访问器可能会访问不正确的内存。

另一个问题是你想从"内部"类访问"外部"内存。 AFAIK这样的东西在C++也是UB。

您如何看待这种方法?

假设它是正确的(事实并非如此),我仍然不喜欢它。你想要getter/setter,但C++不支持此功能。因此,与其做那些奇怪、复杂的结构(想象一下其他人维护这段代码),不如简单地做

struct Vector3f {
float data[3];
float x() {
return data[0];
}
void x(float value) {
data[0] = value;
}
...
};

你说这段代码很丑。也许是吧。但它很简单,易于阅读和维护。没有 UB,它不依赖于工会的潜在黑客,并且完全可以做你想做的事,除了美容要求。:)

GLM 在匿名union中使用匿名struct实现此类功能

我个人不能保证这是符合标准的,但大多数主要的编译器(MSVC,GCC,Clang)都会支持这个习惯用法:

struct Vector3f {
union {
struct {
float x, y, z;
};
struct {
float data[3];
};
};
Vector3f() : Vector3f(0,0,0) {}
Vector3f(float x, float y, float z) : x(x), y(y), z(z) {}
};
int main() {
Vector3f vec;
vec.x = 14.5;
std::cout << vec.data[0] << std::endl; //Should print 14.5
vec.y = -22.345;
std::cout << vec.data[1] << std::endl; //Should print -22.345
std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

非标准行为位于用于将字母组合在一起的匿名结构中,GCC 将对此发出警告。据我所知,union本身应该是有效的,因为数据类型都是相同的,但是如果您不确定这是否有效,您仍然应该检查编译器文档。

为了增加便利,我们还可以重载括号运算符以稍微缩短语法:

struct Vector3f {
/*...*/
float& operator[](size_t index) {return data[index];}
float operator[](size_t index) const {return data[index];}
};

int main() {
Vector3f vec;
vec.x = 14.5;
std::cout << vec[0] << std::endl; //Should print 14.5
vec.y = -22.345;
std::cout << vec[1] << std::endl; //Should print -22.345
std::cout << sizeof(vec) << std::endl; //On most platforms will print 12
}

为了清楚起见,根据C++标准,以我的方式访问非活动成员是有效的,因为这些成员共享一个"公共子序列":

如果两个联合成员是标准布局类型,则在任何编译器上检查其公共子序列都是明确定义的。

CPP参考:工会宣言

因为xdata[0]

  • 两者都float
  • 两者都占用相同的内存,
  • 都是标准定义的标准布局类型,

无论当前哪个处于活动状态,访问一个或另一个都是完全有效的。

如前所述,这是不可能的:指针算术仅在数组中定义,并且没有办法(如果不在类中放置引用,这在当前实现中占用空间)v.x引用数组元素。