这个C++的setter/getter模式破坏了什么?

5

在C++中使用GLSL语法

我编写了自定义向量类,例如vec2vec3等,它们模仿GLSL类型并且大致如下:

struct vec3
{
    inline vec3(float x, float y, float z)
      : x(x), y(y), z(z) {}
    union { float x, r, s; };
    union { float y, g, t; };
    union { float z, b, p; };
};

向量操作的实现方式如下:

inline vec3 operator +(vec3 a, vec3 b)
{
    return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
}

这使我能够使用类似GLSL的语法创建向量并访问其组件,并且几乎可以像处理数字类型一样对它们执行操作。联合体允许我无差别地将第一个坐标称为xr,就像在GLSL中一样。例如:

vec3 point = vec3(1.f, 2.f, 3.f);
vec3 other = point + point;
point.x = other.b;

Swizzling的问题

GLSL也允许使用Swizzling操作,即使在组件之间有空隙。例如,p.yx的行为类似于具有pxy交换的vec2。当没有重复的组件时,它也是一个lvalue。以下是一些例子:

other = point.xyy; /* Note: xyy, not xyz */
other.xz = point.xz;
point.xy = other.xx + vec2(1.0f, 2.0f);

现在可以使用标准的getter和setter来完成这个操作,例如vec2 xy()void xy(vec2 val)。这就是GLM库所做的事情。
透明的getter和setter 然而,我设计了这种模式,让我在C++中完全实现相同的功能。由于所有内容都是POD-struct,因此我可以添加更多联合体:
template<int I, int J> struct MagicVec2
{
    friend struct vec2;
    inline vec2 operator =(vec2 that);

private:
    float ptr[1 + (I > J ? I : J)];
};

template<int I, int J>
inline vec2 MagicVec2<I, J>::operator =(vec2 that)
{
    ptr[I] = that.x; ptr[J] = that.y;
    return *this;
}

例如,vec3类变成了这样(我稍微简化了一下,例如在这里没有阻止xx作为左值使用):

struct vec3
{
    inline vec3(float x, float y, float z)
      : x(x), y(y), z(z) {}

    template<int I, int J, int K>
    inline vec3(MagicVec3<I, J, K> const &v)
      : x(v.ptr[I]), y(v.ptr[J]), z(v.ptr[K]) {}

    union
    {
        struct { float x, y, z; };
        struct { float r, g, b; };
        struct { float s, t, p; };

        MagicVec2<0,0> xx, rr, ss;
        MagicVec2<0,1> xy, rg, st;
        MagicVec2<0,2> xz, rb, sp;
        MagicVec2<1,0> yx, gr, ts;
        MagicVec2<1,1> yy, gg, tt;
        MagicVec2<1,2> yz, gb, tp;
        MagicVec2<2,0> zx, br, ps;
        MagicVec2<2,1> zy, bg, pt;
        MagicVec2<2,2> zz, bb, pp;
        /* Also MagicVec3 and MagicVec4, of course */
    };
};

基本上:我使用联合将向量的浮点分量与一个神奇的对象混合在一起,该对象实际上不是一个vec2,但可以隐式地转换为vec2(因为有一个允许它的vec2构造函数),并且可以赋值给vec2(因为它的重载赋值运算符)。
我对结果非常满意。上面的GLSL代码可以工作,我相信我得到了不错的类型安全性。而且我可以在我的C++代码中#include一个GLSL着色器。
限制:
当然有限制。我知道以下限制:
- sizeof(point.xz)将是3*sizeof(float)而不是预期的2*sizeof(float)。这是设计上的问题,我不知道这是否会有问题。 - &foo.xz不能用作vec2*。这应该没问题,因为我只通过值传递这些对象。
所以我的问题是:我可能忽略了什么,这会使我的生活难以忍受吗?此外,我还没有在其他地方找到这种模式,所以如果有人知道它的名称,我很感兴趣。
注意:我希望坚持使用C++98,但我确实依赖于编译器允许通过联合进行类型戏弄。我不想使用C++11的原因是我的目标平台中有几个缺乏编译器支持;所有对我有兴趣的编译器都支持类型戏弄。

2
因为您正在使用C++98(C++11没有此问题),所以当您从联合中读取不是最后一个写入该联合的值时,您正在调用UB。 - Seth Carnegie
@SethCarnegie:你说得对,我确实依赖于编译器明确支持这一点;我正在添加一个注释。 - sam hocevar
我没有看到任何适合作为正确getter的东西; 给定一个vec3 v1,似乎没有办法将v1.xz正确地分配给实际的vec2 v2,因为它看起来像是没有重新排序元素(v1.z必须进入v2.y,这是向量中的不同偏移)。你在帖子中漏掉了吗,还是我错过了什么? - Michael Madsen
@MichaelMadsen:请看vec3::vec3(MagicVec3<>const&)构造函数中的重新排序方式。 为了简洁起见,我省略了vec2的相应构造函数,但它当然也存在。由于此构造函数不是explicit,因此任何接受vec2的方法也将隐式接受MagicVec2。 因此,转换被延迟,直到实际需要其内容。 - sam hocevar
@SethCarnegie:联合体的所有成员都具有相同的类型,因此它们的地址可能会别名,因此我认为它是已定义的。据我所知,需要编译器支持的是使用不同类型读取/写入不同的联合成员。 - Pierre Habouzit
一个有趣的事实,可能与此相关(我把判断留给你),就是图形硬件没有不同的向量大小,大多数API也是如此。硬件有一组float4寄存器,设置“float2”会设置其前两个分量。着色器编译器和优化器以及作者和艺术家都广泛使用这个属性,将变量和数据打包到额外的分量中。通过将所有内容都设置为float4并填充/忽略未使用的分量,您将大大简化生活。 - ssube
2个回答

3
简而言之:我认为确保这种模式能够正常工作很困难,这就是你在询问的原因。此外,这种模式可以被标准代理模式替换,其正确性更容易保证。不过,我必须承认,基于代理的解决方案的存储开销在静态创建代理时是一个问题。
上述代码的正确性
这是一段没有明显错误的代码;但用C. A. R. Hoare的话来说,这不是没有明显错误的代码。此外,让自己相信没有错误有多难? 我看不出这种模式不起作用的原因-但要证明它将起作用并不容易(即使是非正式的)。实际上,尝试进行证明可能会失败,并指出一些问题。 为了安全起见,我会禁用MagicVecN类的所有隐式生成的构造函数/赋值运算符,以避免考虑所有相关的复杂性(见下面的子段); 但是这样做是被禁止的,因为对于union成员,无法覆盖隐式定义的复制赋值运算符,正如我拥有的标准草案和GCC的错误消息所解释的那样:
member ‘MagicVec2<0, 0> vec3::<anonymous union>::xx’ with copy assignment operator not allowed in union

在附加的gist中,我提供了一个手动实现来确保安全。
请注意,MagicVec2的赋值运算符应该通过const引用接受其参数(参见下面的示例,其中这个方法有效); 隐式转换仍会发生(const引用将指向创建的临时对象;如果没有const限定词,则此方法无法工作)。
几乎有问题,但还不完全。
我本以为发现了一个bug(其实不是),但考虑这个问题还是有点意思的——只是为了看看需要涵盖多少情况才能排除潜在的bug。 p.xz = p.zx会产生正确的结果吗?我认为会调用MagicVec2的隐式赋值运算符,导致错误的结果;实际上它并没有(我相信),因为IJ是不同的,并且是类型的一部分。如果类型相同呢? p.xx = q.rr是安全的,但p.xx = p.rr很棘手(即使可能很愚蠢,但它仍然不应该破坏内存):隐式生成的赋值运算符是基于memcpy的吗?答案似乎是否定的,但如果是肯定的,这将是在重叠的内存区间之间进行memcpy,这是未定义行为。
更新:一个实际的问题

正如OP所注意到的那样,对于表达式p.xz = q.xz,默认的复制赋值运算符也会被调用;在这种情况下,它实际上也会复制.y成员。如上所述,对于属于联合体的数据类型,无法禁用或修改复制赋值运算符。

代理模式

此外,我认为有一个更简单的解决方案,即代理模式(部分使用)。MagicVecX应该包含指向包含类的指针,而不是ptr;这样你就不需要使用联合体的技巧了。

template<int I, int J> struct MagicVec2
{
    friend struct vec2;
    inline MagicVec2(vec2* _this): ptr(_this) {}
    inline vec2 operator=(const vec2& that);
private:
    float *ptr;
};

我通过编译(但未链接)这段代码进行了测试,该代码概述了提出的解决方案: https://gist.github.com/1775054。请注意,该代码不完整也未经过测试 - 还应该重写MagicVecX的复制构造函数。


非常感谢你的彻底分析、建议和修复!我不确定是否应该在问题中更新代码,但是我根据你的建议在我的个人代码库中将operator=修复为使用一个const引用。回答你的一些观点:p.xz = p.zx能工作是因为编译器被迫创建临时的Vec2。赋值p.xx应该不允许;我通过将xx和其他变量标记为const来解决这个问题。但是你提出了一个有趣的观点,即由于隐式赋值运算符,p.xz = q.xz会破坏p.y。这确实是一个真正的错误! - sam hocevar
顺便说一下,我不想使用代理模式的原因是它会改变对象的大小——因为在某些平台上,例如vec4被别名为4个浮点数的MMX或AltiVec向量,我不能让它的大小为除了16字节之外的任何东西。 - sam hocevar
好像整个模式都崩溃了,因为C++98不允许我禁用隐式赋值运算符。你能否更清楚地说明 p.xz = q.xz 会失败,这样我就可以接受你的答案了吗?谢谢。 - sam hocevar
其实我没有考虑过 'p.xz = q.xz',尽管我说过隐式赋值运算符可能是个问题。 - Blaisorblade
我对答案进行了小更新。现在重写它会更有意义,但需要花费太多时间。 - Blaisorblade
你的贡献无疑对于找到那个 bug 至关重要。虽然我非常失望这个模式不起作用,但赏金归你 :-) 谢谢。 - sam hocevar

3

好的,我已经发现一个问题,虽然与上面的代码直接无关。如果vec3被改成模板类以支持例如intfloat之类的类型,那么+操作符将变成:

template<typename T>
inline vec3<T> operator +(vec3<T> a, vec3<T> b)
{
    return vec3<T>(a.x + b.x, a.y + b.y, a.z + b.z);
}

那么这段代码将不能工作:
vec3<float> a, b, c;
...
c = a.xyz + b;

原因在于计算+的参数需要进行模板参数推导(T = float) 一个隐式转换(从MagicVec3<T,0,1,2>vec3<T>,这是不允许的。

然而,有一种对我可接受的解决方案:编写所有可能的显式运算符。

inline vec3<int> operator +(vec3<int> a, vec3<int> b)
{
    return vec3<int>(a.x + b.x, a.y + b.y, a.z + b.z);
}

inline vec3<float> operator +(vec3<float> a, vec3<float> b)
{
    return vec3<float>(a.x + b.x, a.y + b.y, a.z + b.z);
}

这还可以让我定义隐式推导的规则,例如我可以决定 vec3<float> + vec3<int> 是合法的,并返回一个 vec3<float>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接