我在这里滥用了模板吗?

Have I misused templates here?

本文关键字:在这里      更新时间:2023-10-16

我试图为一个网格类进行通用编程。 (我需要 CPU 转换来变形网格,这就是为什么这不会在 GPU 中完成,以防您想问)

Mesh<T>包含 3D 形状的顶点,以及面绑定(哪些顶点连接在一起形成三角形)。

这里T是顶点的类型,某些类型的模型具有PNCT类型(位置,法线,颜色,texcoord),而其他模型只是PC(位置,颜色)。

struct VertexPNCT {
Vector3 pos, normal ;
Vector4 color ;
Vector2 tex ;
} ;
struct VertexPC {
Vector3 pos ;
Vector4 color ;
} ;

当然,所有顶点都有一个位置!

但这是我的问题。

Mesh<T>必须实现transform方法。 当然,每个顶点格式都有一个位置,如果它被一致地命名(.pos),那么该方法就可以正常工作:

由于每个顶点肯定都有一个位置,如果我总是在Vertex*结构中调用该成员.pos,那么我可以添加一个方法.transform到模板化类Mesh<T>为:

void transform( Matrix4& mat )
{
each vertex
vertex.pos = mat * vertex.pos ; // transform it
}

现在搞砸了。如果顶点格式中有法线,那么该法线也必须被转换(通过"法线矩阵")。 现在我的模板中有一个"if 语句"? 我必须为每个顶点类型编写一个模板专用化.transform方法(基本上分为 2 个类别,有法线的和没有法线的)?

我在这里滥用了模板吗? 错过了一些"编程船"?

这是我真正想在逻辑上做的事情:

void transform( Matrix4& mat )
{
each vertex in vertices
{
vertex.pos = mat * vertex.pos ; // transform it
#ifdef vertex.normal
vertex.normal = mat * vertex.normal ; // transform normal as well*
#endif
}
}

* 假设转换没有规模 (那么你不需要使用"普通矩阵")

如果你可以编写一个Vertex层次结构,其中基只保存位置(和.pos成员),一个VertexWNormal保存.normal,然后其余部分从那里继承,然后你可以添加非模板化的重载,让编译器处理:

void transform( Matrix4& m, VertexBase& v ) {
// transform only pos
}
void transform( Matrix4& m, VertexWNormal& v ) {
transform(m,static_cast<VertexBase&>(v));
// transform normal here
}
void tranform( Matrix4& m ) {
foreach vertex:
transform(m,vertex);
}

当然,这对你的设计是否有意义取决于你没有展示的很多东西,很难说。

如果你只有两种类型的顶点,我建议你按照大卫上面描述的去做: 只需创建两个执行转换的函数,并根据顶点类型使用重载调用它们。这也适用于更多的顶点类型,但每次添加新的顶点类型时,都需要再次重载函数。对于此处描述的简单函数来说,这可能是可以的,但如果函数实际上更复杂,则可能会变得烦人。

部分修复是创建一个特征类,该类告诉特定顶点类型是否具有普通成员。可以设置默认值,使其在大多数情况下是正确的,并且可以根据特征选择合适的函数。您仍将提供两个版本的代码,但对于每个额外的顶点类型,只需要定义特征:

template <typename> struct VertexHasNormal { enum { value = false }; };
template <> struct VertexHasNormal<VertexPNCT> { enum { value = true }; };
template <typename V, template T, template S>
void transform(V& vertices, Matrix4& m, T S::*member) {
for (auto& v: vertices) {
v.*member = mat * v.*member;
}
}
template <bool, typename T>
struct Transform {
template <typename V>
void call(V& vertices, Matrix4& m) {
transform(vertices, m, &T::pos);
}
};
template <typename T>
struct Transform<bool, T> {
template <typename V>
void call(V& vertices, Matrix4& m) {
transform(vertices, m, &T::pos);
transform(vertices, m, &T::normal);
}
};
template <typename T>
void Mash<T>::transform(Matrix4& m) {
Transform<VertexHasNormal::value>::call(this->vertices, m);
}

函数模板transform()是对顶点序列执行实际转换的函数。我考虑它是因为我认为它可以被分解出来有点可爱,但它不需要被分解掉。它也不需要使用指向成员、auto等的指针。Transform类型只是使用的辅助类型,因为函数模板不能部分专用化。它专门研究顶点类型是否具有normal成员的特征。最后,Mash<T>::transform()只是调度到专用函数的相应版本。

唯一需要做的是在定义另一个具有normal成员的顶点时添加新的特征专用化。这可能不可取。在这种情况下,可以使用类型特征确定结构是否具有名为normal的可访问数据成员。但是,我认为我不能从头顶上输入这个。实现它的基本思想是利用替换失败不是错误的事实(" SFINAE),并且如果测试类型确实具有必要的成员,则可以设置某些内容以在两个潜在成员之间创建歧义。有一个 Boost 组件可以做到这一点,但如果您需要自己创建它,大约需要 10 行代码。

您可能希望创建结构类并从两个基类派生它们,一个具有普通基类,一个没有基类。然后,可以使用模板专用化在这两个基类(而不是所有顶点类)之间进行选择。