方便的 Vector3f class

convenient Vector3f class

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

类似于:

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 中使用 space(这很不幸。我看不出在这种特殊情况下不能删除引用的任何原因,也许它错过了编译器部分的优化。

但是,有了 [[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 的访问。这些属性不应采用 space,因为它们是空的,并且具有 [[no_unique_address]]

的属性

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


注意,这个问题是关于一个 class,所有这些都是可能的:

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

GLM 在匿名 union

中使用匿名 structs 来实现这种功能

我个人不能保证这符合标准,但大多数主要编译器(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++ 标准,以我的方式访问非活动成员是有效的,因为这些成员共享一个“公共子序列”:

If two union members are standard-layout types, it's well-defined to examine their common subsequence on any compiler.

CPP Reference: Union Declaration

因为xdata[0]

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

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

如果这将在 header 中存在,并且您对编译器的优化能力有一定的信心,您可以坚持使用 plain-old operator[]() 重载并期待编译器足够聪明,可以省略调用和 return 您想要的元素。例如:

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 处的 operator[] 调用,以及 -O3 处的 GCC。不如您的方法令人兴奋,但简单并且在大多数情况下应该有效。

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

But this implementation is bad, because references take space in the struct (which is quite unfortunate. I don't see any reason why references cannot be removed in this particular case, maybe it is missed optimization from the compiler's part).

这看起来是个复杂的问题。标准布局 classes 必须相互兼容。因此,编译器不允许删除任何成员,无论它们是如何定义的。对于非标准布局?谁知道。欲了解更多信息,请阅读:Do the C++ standards guarantee that unused private fields will influence sizeof?

根据我的经验,编译器永远不会删除 class 成员,即使它们是 "unused"(例如 sizeof 正式使用它们)。

Does it have UB?

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

另一个问题是你想从"inner"class访问"outer"内存。 AFAIK 这样的东西在 C++ 中也是 UB。

What do you think about this approach?

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

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

你说这段代码很丑。也许是。但它很简单,易于阅读和维护。没有 UB,它不依赖于与工会的潜在黑客攻击,并且完全按照你的意愿行事,除了美观要求。 :)