做出根据类型执行不同代码。要达成这一目的,模板并不是唯一的途径。比如之前我们所说的重载。如果把眼界放宽一些,虚函数也是根据类型执行代码的例子。此外,在C语言时代,也会有一些技法来达到这个目的,比如下面这个例子,我们需要对两个浮点做加法, 或者对两个整数做乘法:
struct Variant
{
union
{
int x;
float y;
} data;
uint32 typeId;
};
Variant addFloatOrMulInt(Variant const* a, Variant const* b)
{
Variant ret;
assert(a->typeId == b->typeId);
if (a->typeId == TYPE_INT)
{
ret.x = a->x * b->x;
}
else
{
ret.y = a->y + b->y;
}
return ret;
}
更常见的是 void*:
#define BIN_OP(type, a, op, b, result) (*(type *)(result)) = (*(type const *)(a)) op (*(type const*)(b))
void doDiv(void* out, void const* data0, void const* data1, DATA_TYPE type)
{
if(type == TYPE_INT)
{
BIN_OP(int, data0, *, data1, out);
}
else
{
BIN_OP(float, data0, +, data1, out);
}
}
在C++中比如在 Boost.Any 的实现中,运用了 typeid 来查询类型信息。和 typeid 同属于RTTI机制的 dynamic_cast,也经常会用来做类型判别的工作。我想你应该写过类似于下面的代码:
IAnimal* animal = GetAnimalFromSystem();
IDog* maybeDog = dynamic_cast<IDog*>(animal);
if(maybeDog)
{
maybeDog->Wangwang();
}
ICat* maybeCat = dynamic_cast<ICat*>(animal);
if(maybeCat)
{
maybeCat->Moemoe();
}
当然,在实际的工作中,我们建议把需要 dynamic_cast 后执行的代码,尽量变成虚函数。不过这个已经是另外一个问题了。我们看到,不管是哪种方法都很难避免 if 的存在。而且因为输入数据的类型是模糊的,经常需要强制地、没有任何检查的转换成某个类型,因此很容易出错。
但是模板与这些方法最大的区别并不在这里。模板无论其参数或者是类型,它都是一个编译期分派的办法。编译期就能确定的东西既可以做类型检查,编译器也能进行优化,砍掉任何不必要的代码执行路径。例如在上例中,
template <typename T> T addFloatOrMulInt(T a, T b);
// 迷之代码1:用于T是float的情况
// 迷之代码2:用于T是int时的情况
如果你运用了模板来实现,那么当传入两个不同类型的变量,或者不是 int 和 float 变量,编译器就会提示错误。但是如果使用了我们前述的 Variant 来实现,编译器可就管不了那么多了。但是,成也编译期,败也编译期。最严重的“缺点”,就是你没办法根据用户输入或者别的什么在运行期间可能发生变化的量来决定它产生、或执行什么代码。比如下面的代码段,它是不成立的。
template <int i, int j>
int foo() { return i + j; }
int main()
{
cin >> x >> y;
return foo<x, y>();
}
这点限制也粉碎了妄图用模板来包办工厂(Factory)甚至是反射的梦想。尽管在《Modern C++ Design》中(别问我为什么老举这本书,因为《C++ Templates》和《Generic Programming》我只是囫囵吞枣读过,基本不记得了)大量运用模板来简化工厂方法;同时C++11/14中的一些机制如Variadic Template更是让这一问题的解决更加彻底。但无论如何,直到C++11/14,光靠模板你就是写不出依靠类名或者ID变量产生类型实例的代码。
所以说,从能力上来看,模板能做的事情都是编译期完成的。编译期完成的意思就是,当你编译一个程序的时候,所有的量就都已经确定了。比如下面的这个例子:
int a = 3, b = 5;
Variant aVar, bVar;
aVar.setInt(a); // 我们新加上的方法,怎么实现的无所谓,大家明白意思就行了。
bVar.setInt(b);
Variant result = addFloatOrMulInt(aVar, bVar);
除非世界末日,否则这个例子里不管你怎么蹦跶,单看代码我们就能知道, aVar 和 bVar 都一定会是整数。所以如果有合适的机制,编译器就能知道此处的 addFloatOrMulInt 中只需要执行 Int 路径上的代码,而且编译器在此处也能单独为 Int 路径生成代码,从而去掉那个不必要的 if。
在模板代码中,这个“合适的机制”就是指“特化”和“部分特化(Partial Specialization)”,后者也叫“偏特化”。