这个 C++ 是什么意思? setter/getter 模式中断?
使用 C++ 中的 GLSL 语法,
我编写了自定义向量类,例如 vec2
、vec3
等,它们模仿 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 的语法访问它们的组件,并对它们执行操作几乎就像它们是数字类型一样。并集允许我将第一个坐标无差别地称为 x
或 r
,就像 GLSL 中的情况一样。例如:
vec3 point = vec3(1.f, 2.f, 3.f);
vec3 other = point + point;
point.x = other.b;
swizzling 的问题
但是 GLSL 也允许 swizzled 访问,即使组件之间存在漏洞。例如,p.yx
的行为类似于带有 p
的 x
和 y
的 vec2
> 交换了。当没有重复组件时,它也是左值。一些示例:
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 结构,因此我可以添加更多联合:
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 的原因是我的几个目标平台缺乏编译器支持;不过,我感兴趣的所有编译器都支持类型双关。
Using the GLSL syntax in C++
I wrote custom vector classes such as vec2
, vec3
etc. that mimic the GLSL types and look roughly like this:
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; };
};
Operations on vectors are implemented this way:
inline vec3 operator +(vec3 a, vec3 b)
{
return vec3(a.x + b.x, a.y + b.y, a.z + b.z);
}
This allows me to create vectors and access their components using a GLSL-like syntax and perform operations on them almost as if they were numeric types. The unions allow me to refer to the first coordinate indifferently as x
or as r
, as is the case in GLSL. For instance:
vec3 point = vec3(1.f, 2.f, 3.f);
vec3 other = point + point;
point.x = other.b;
The problem of swizzling
But GLSL also allows swizzled access, even with holes between components. For instance p.yx
behaves like a vec2
with p
’s x
and y
swapped. When no component is repeated, it is also an lvalue. Some examples:
other = point.xyy; /* Note: xyy, not xyz */
other.xz = point.xz;
point.xy = other.xx + vec2(1.0f, 2.0f);
Now this could be done using standard getters and setters such as vec2 xy()
and void xy(vec2 val)
. This is what the GLM library does.
Transparent getter and setter
However, I devised this pattern that lets me do exactly the same in C++. Since everything is a POD-struct, I can add more unions:
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;
}
And eg. the vec3
class becomes (I simplified things a bit, for instance nothing prevents xx
from being used as an lvalue here):
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 */
};
};
Basically: I use a union to mix the vector’s floating-point components with a magic object which is not really a vec2
but can be cast implicitly to a vec2
(because there’s a vec2
constructor allowing it), and can be assigned a vec2
(because of its overloaded assignment operator).
I am very satisfied with the result. The GLSL code above works and I believe I get decent type safety. And I can #include
a GLSL shader in my C++ code.
Limitations
Of course there are limitations. I know of the following ones:
sizeof(point.xz)
will be3*sizeof(float)
instead of the expected2*sizeof(float)
. This is by design and I do not know whether this could be problematic.&foo.xz
cannot be used as avec2*
. This should be OK because I only ever pass these objects by value.
So my question is: what may I have overlooked that will make my life difficult with this pattern? Also, I have not found this pattern anywhere else yet, so if anyone knows its name I am interested.
Note: I wish to stick to C++98, but I do rely on the compiler allowing type-punning through unions. My reason for not wanting C++11 yet is the lack of compiler support on several of my target platforms; all the compilers that are of interest to me support type punning, though.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
简而言之:我认为很难确保这种模式有效 - 这就是你问的原因。此外,该模式可以替换为标准代理模式,其正确性更容易保证。我不得不承认,当静态创建代理时,基于代理的解决方案的存储开销是一个问题。
上述代码的正确性
这是没有明显错误的代码;但解释CAR Hoare,这不是明显没有错误的代码。而且,要说服自己没有bug有多难?
我看不出该模式不起作用的任何原因 - 但要证明(即使是非正式的)它会起作用并不那么容易。事实上,尝试进行证明可能会失败并指出一些问题。
为了安全起见,我会禁用 MagicVecN 类的所有隐式生成的构造函数/赋值运算符,以避免考虑所有相关的复杂性(请参阅下面的小节);然而这样做是被禁止的,因为对于联合成员来说,不能覆盖隐式定义的复制赋值运算符,正如我拥有的标准草案和 GCC 的错误消息所解释的那样:
在附加的要点中,为了安全起见,我改为手动提供一个实现。
请注意,MagicVec2 的赋值运算符应通过 const 引用接受其参数(请参阅下面的示例,了解其工作原理);隐式转换仍然会发生(const 引用将指向创建的临时对象;如果没有 const 限定符,这将不起作用)。
几乎是问题,但不完全是
我认为发现了一个错误(我没有),但考虑一下仍然有点有趣 - 只是看看必须覆盖多少个案例才能排除潜在的错误。
p.xz = p.zx
会产生正确的结果吗?我认为 MagicVec2 的隐式赋值运算符会被调用,从而导致不正确的结果;事实上,它不是(我相信),因为I
和J
是不同的并且是类型的一部分。当类型相同时怎么办?p.xx = q.rr
是安全的,但p.xx = p.rr
很棘手(尽管它可能很愚蠢,但它仍然不会损坏内存) :隐式生成的赋值运算符是基于memcpy的吗?答案似乎是否定的,但如果是的话,这将是重叠内存间隔之间的memcpy,这是未定义的行为。更新:一个实际问题
正如OP所注意到的,默认的复制赋值运算符也会被表达式
p.xz = q.xz
调用;在这种情况下,它实际上也会复制.y
成员。如上所述,对于属于联合一部分的数据类型,不能禁用或修改复制赋值运算符。代理模式
此外,我相信有一个更简单的解决方案,即代理模式(您正在部分使用)。
MagicVecX
应包含指向包含类的指针,而不是ptr
;这样你就不需要使用工会的技巧了。我通过编译(但不链接)此代码进行了测试,该代码概述了建议的解决方案:https://gist.github。 com/1775054。请注意,代码不完整,也未经过测试 - 还应该重写
MagicVecX
的复制构造函数。In short: I think that it is difficult to make sure that this pattern works - that's why you are asking. Moreover, this pattern could be replaced by a standard proxy pattern, for which correctness is easier to guarantee. I have to admit though that the storage overhead of a proxy-based solution is a problem when the proxies are created statically.
Correctness of the above code
This is code where there is no obvious bug; but paraphrasing C. A. R. Hoare, this is not code where there is obviously no bug. Moreover, how hard is it to convince oneself that there is no bug?
I do not see any reason why the pattern would not work - but it is not so easy to prove (even informally) that it will work. In fact, trying doing a proof could fail and point out to some problems.
To be safe, I would disable all implicitly-generated constructors/assignment operators for
MagicVecN
classes, just to avoid considering all the associated complications (see subsection below); however doing that is forbidden, because for union members one cannot override the implicitly defined copy assignment operator, as explained by the standard draft I have and by GCC's error message:In the attached gist, I instead provide an implementation manually to be safe.
Note that MagicVec2's assignment operator should accepts its parameter by const reference (see example below, where this works); implicit conversions still happen (the const reference will point to the created temporary; this would not work without the const qualifier).
Almost problems, but not quite
I thought a found a bug (which I didn't), but it is still somewhat interesting to consider - just to see how many cases must be covered to rule out potential bugs. Would
p.xz = p.zx
produce the correct results? I thought thatMagicVec2
's implicit assignment operator would be invoked, leading to incorrect results; in fact, it isn't (I believe) becauseI
andJ
are different and part of the type. What when the type is the same?p.xx = q.rr
is safe, butp.xx = p.rr
is tricky (even though it might be stupid, but it should still not corrupt memory): is the implicitly-generated assignment operatormemcpy
-based? The answer seems to be no, but if yes, this would be amemcpy
between overlapping memory intervals, which is undefined behavior.UPDATE: An actual problem
As noticed by the OP, the default copy assignment operator is also invoked for the expression
p.xz = q.xz
; in that case, it will in fact also copy the.y
member. As mentioned above, the copy assignment operator cannot be disabled or modified for datatypes which are part of an union.The proxy pattern
Moreover, I believe that there is a much simpler solution, namely the proxy pattern (which you are partially using).
MagicVecX
should contain a pointer to the containing class instead ofptr
; this way you need no trick using unions.I tested this by compiling (but not linking) this code, which sketches the proposed solution: https://gist.github.com/1775054. Note that the code is not complete nor tested - one should also override the copy constructor of
MagicVecX
.好吧,我已经发现了一个问题,尽管不是直接使用上面的代码。如果以某种方式将 vec3 制作为模板类,以便除了
float
之外还支持eg.int
,并且+
运算符变为:那么此代码将不起作用:
原因是计算
+
的参数将需要模板参数推导 (T = float
) 和隐式转换(来自MagicVec3
到vec3
,这是不允许的,但是有一个我可以接受的解决方案:编写所有可能的显式运算符。
这也让我可以定义隐式提升的规则,例如我可以确定 vec3+ vec3是合法的并且将返回
vec3
。Okay, I have found one problem already, though not directly with the code above. If
vec3
is somehow made a template class in order to support eg.int
in addition tofloat
, and the+
operator becomes:Then this code will not work:
The reason is that figuring the arguments for
+
will require both template argument deduction (T = float
) and an implicit conversion (fromMagicVec3<T,0,1,2>
tovec3<T>
, which is not allowed.There is however a solution acceptable for me: write all possible explicit operators.
This will also let me define rules for implicit promotion, for instance I can decide that
vec3<float> + vec3<int>
is legal and will return avec3<float>
.