以符合标准的方式使用与数组相同类型的成员重新解释结构

Reinterpret struct with members of the same type as an array in a standard compliant way

在各种 3d 数学代码库中,我有时会遇到这样的事情:

struct vec {
    float x, y, z;

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return (&x)[i];
    }
};

其中,AFAIK 是非法的,因为允许实现在成员之间虚假地添加填充,即使它们属于同一类型,尽管 none 实际上会这样做。

是否可以通过 static_asserts 施加约束使其合法?

static_assert(sizeof(vec) == sizeof(float) * 3);

static_assert 未被触发是否意味着 operator[] 执行预期的操作并且在运行时不调用 UB?

如何将数据成员存储为数组并通过名称访问它们?

struct vec {
    float p[3];

    float& x() { return p[0]; }
    float& y() { return p[1]; }
    float& z() { return p[2]; }

    float& operator[](std::size_t i)
    {
        assert(i < 3);
        return p[i];
    }
};

编辑:对于最初的方法,如果 x、y 和 z 都是您拥有的成员变量,那么该结构将始终是 3 个浮点数的大小,因此 static_assert 可用于检查operator[] 将在限定大小内访问。

另请参阅:

编辑 2:就像 Brian 在另一个答案中所说的那样,(&x)[i] 本身就是标准中未定义的行为。但是,鉴于 3 个浮点数是唯一的数据成员,此上下文中的代码应该是安全的。

迂腐语法正确性:

struct vec {
  float x, y, z;
  float* const p = &x;

  float& operator[](std::size_t i) {
    assert(i < 3);
    return p[i];
  }
};

尽管这会将每个 vec 增加一个指针的大小。

不,这是不合法的,因为在将整数添加到指针时,以下内容适用 ([expr.add]/5):

If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

y 占用了 x 末尾后一位的内存位置(被认为是一个只有一个元素的数组)所以定义了 &x 加 1,但是 &x 加 2 =12=] 未定义。

您永远无法确定这是否有效

不能保证后续成员的连续性,即使由于通常的浮点对齐属性和允许的指针算法,这在实践中经常可以完美地工作。

这是在 C++ 标准的以下条款中规定的:

[class.mem]/18: Non-static data-members (...) with the same access control are allocated so that later members have higher addresses within the class object. Implementation alignment requirements might cause two adjacent members not to be allocated after each other.

无法使用 static_assertalignas 约束来使其合法。你所能做的就是防止编译,当元素不连续时,使用属性每个对象的地址是唯一的:

    static_assert (&y==&x+1 && &z==&y+1, "PADDING in vector"); 

但您可以重新实现运算符以使其符合标准

一个安全的替代方案是重新实现 operator[] 以消除三个成员的连续性要求:

struct vec {
    float x,y,z; 

    float& operator[](size_t i)
    {
        assert(i<3); 
        if (i==0)     // optimizing compiler will make this as efficient as your original code
            return x; 
        else if (i==1) 
            return y; 
        else return z;
    }
};

请注意,优化编译器将为重新实现和您的原始版本生成非常相似的代码(参见 an example here)。所以宁愿选择兼容的版本。

类型别名(对基本相同的数据使用多种类型)是 C++ 中的一个大问题。如果您将成员函数保留在结构之外并将它们维护为 PODs,事情应该会起作用。但是

  static_assert(sizeof(vec) == sizeof(float) * 3);

不能使访问一种类型在技术上合法。在实践中当然不会有填充,但 C++ 还不够聪明,无法意识到 vec 是一个浮点数组,而 vecs 数组是一个约束为三的倍数的浮点数组,并且转换 &vecasarray[0 ] 到 vec * 是合法的,但强制转换 &vecasarray[1] 是非法的。

根据标准,这显然是未定义行为,因为您要么在数组外部进行指针运算,要么为结构和数组的内容设置别名。

问题是math3D代码可以密集使用,底层优化有意义。符合 C++ 的方式是直接存储数组,并使用访问器或对数组的各个成员的引用。这两个选项都不是很好:

  • 访问器:

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x() & { return arr[0];}
        float& y() & { return arr[1];}
        float& z() & { return arr[2];}
    };
    

    问题是,使用函数作为左值对于老 C 程序员来说并不自然:v.x() = 1.0; 确实是正确的,但我宁愿避免使用会迫使我编写它的库。当然我们可以使用 setter,但如果可能的话,我更喜欢写 v.x = 1.0; 而不是 v.setx(1.0);,因为常见的习语 v.x = v.z = 1.0; v.y = 2.0;。这只是我的意见,但我发现它比 v.x() = v.z() = 1.0; v.y() = 2.0;v.setx(v.sety(1.0))); v.setz(2.0);.

  • 更整洁
  • 引用

    struct vec {
    private:
        float arr[3];
    public:
        float& operator[](std::size_t i)
        {
            assert(i < 3);
            return arr[i];
        }
        float& x;
        float& y;
        float& z;
        vec(): x(arr[0]), y(arr[1]), z(arr[2]) {}
    };
    

    不错!我们可以写 v.xv[0],它们都代表相同的内存......不幸的是,编译器仍然不够聪明,无法看到 refs 只是一个 in struct 数组的别名和struct 是数组大小的两倍!

由于这些原因,不正确的别名仍然很常用...