9

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

struct vec {
    float x, y, z;

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

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

通过static_asserts 施加约束可以使其合法化吗?

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

static_assert没有被触发意味着operator[]什么是预期的并且在运行时不调用UB?

4

5 回答 5

6

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

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

y占据内存位置的末尾x(被视为具有一个元素的数组),因此&x定义了加 1,但&x未定义加 2。

于 2017-01-01T21:32:36.357 回答
2

你永远不能确定这会奏效

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

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

[class.mem]/18:分配具有相同访问控制的非静态数据成员(...),以便以后的成员在类对象中具有更高的地址。实现对齐要求可能会导致两个相邻的成员不被分配到彼此之后。

没有办法使这种合法使用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;
    }
};

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

于 2017-01-01T21:40:51.473 回答
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[1];}
        float& z() & { return arr[2];}
    };
    

    问题是,对于老 C 程序员来说,将函数用作左值并不自然:v.x() = 1.0;确实是正确的,但我宁愿避免使用会迫使我编写它的库。当然我们可以使用 setter,但是如果可能的话,我更喜欢写v.x = 1.0;than v.setx(1.0);,因为常见的 idiom v.x = v.z = 1.0; v.y = 2.0;。这只是我的意见,但我觉得它比v.x() = v.z() = 1.0; v.y() = 2.0;or更整洁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.xand v[0],它们都代表相同的内存......不幸的是,编译器仍然不够聪明,无法看到 ref 只是 in struct 数组的别名,并且 struct 的大小是数组大小的两倍!

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

于 2017-01-02T12:41:42.117 回答
2

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

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

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

于 2017-01-01T22:24:14.313 回答
-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 是您拥有的所有成员变量,则结构将始终为 3 个浮点数的大小,因此static_assert可用于检查是否operator[]会在有界大小内访问。

另请参阅:C++ 结构成员内存分配

编辑2:就像布赖恩在另一个答案中所说,(&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 增加一个指针的大小。

于 2017-01-01T21:29:02.693 回答