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

时间:2017-01-01 21:24:24

标签: c++ language-lawyer strict-aliasing type-punning structure-packing

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

struct vec {
    float x, y, z;

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

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

通过static_assert s施加约束可以使其成为合法吗?

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

即。 static_assert没有被触发意味着operator[]做了什么,并且没有在运行时调用UB?

5 个答案:

答案 0 :(得分:6)

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

  

如果指针操作数和结果都指向同一个数组对象的元素,或者一个过去的元素   数组对象的最后一个元素,评估不应产生溢出;否则,行为是   未定义。

y占用x末尾的内存位置(被视为包含一个元素的数组),因此定义了向&x添加1,但向{{1}添加了2未定义。

答案 1 :(得分:3)

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

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

使用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)。所以请选择合规版本。

答案 2 :(得分:2)

根据标准,它显然是未定义的行为,因为你要么在数组之外做指针算术,要么别名结构和数组的内容。

问题是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[0];}
        float& z() { return arr[0];}
    };
    

    问题在于使用函数作为左值对于旧的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只是结构数组的别名和大小的struct的大小是数组的两倍!

出于这些原因,仍然常用错误的别名......

答案 3 :(得分:1)

类型别名(对于基本相同的数据使用多于一种类型)是C ++中的一个大问题。如果你将成员函数保留在结构体之外并将它们保存为POD,那么事情应该有效。但

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

无法将某种类型作为另一种技术合法访问。在实践中当然没有填充,但是C ++并不聪明,不能认识到vec是一个浮点数组,而vecs数组是一个浮点数组,被限制为三个的倍数,以及cast&amp; vecasarray [0]到vec *是合法的但是cast&amp; vecasarray [1]是非法的。

答案 4 :(得分:-3)

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

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都是你拥有的所有成员变量,那么struct总是3个浮点数的大小,因此static_assert可用于检查{{ 1}}将在有限大小内访问。

另请参阅:C++ struct member memory allocation

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

对语法正确性嗤之以鼻:

(&x)[i]

虽然这会使每个vec增加一个指针的大小。