![]() |
Generic<Programming>:类型和值之间的映射
Andrei Alexandrescu
在C++中,术语“转化”(conversion)描述的是从另外一个类型的值(value)获取一个类型(type)的值的过程。可是有时候你会需要一种不同类型的转化:可能是在你有一个类型时需要获取一个值,或是其它的类似情形。在C++中做这样的转化是不寻常的,因为类型域和值域之间隔有有一堵很严格的界线。可是,在一些特定的场合,你需要跨越这两个边界,本栏就是要讨论该怎么做到这个跨越。
映射整数为类型
一个对许多的generic programming编程风格非常有帮助的暴简单的模板:
template <int v>
struct Int2Type
{
enum { value = v };
};
对传递的每一个不同的常整型值,Int2Type“产生”一个不同的类型。这是因为不同的模板的实体(instantiation)是不同的类型,所以Int2Type<0>不同于Int2Type<1>等其它的类型的。此外,产生类型的值被“存放”在枚举(enum)的成员值里面。
不管在任何时候,只要你需要快速“类型化”(typify)一个整型常数时,你都可以使用Int2Type。比如这个例子,你要设计一个NiftyContainer类模板。
template <typename T>
class NiftyContainer
{
...
};
NiftyContainer存储了指向T的指针。在NiftyContainer的一些成员函数(member functions)中,你需要克隆类型 T的对象,如果T是一个非多态的类型,你可能会这样说:
T* pSomeObj = ...;
T* pNewObj = new T(*pSomeObj);
对于T是多态类型的情形,情况要更为复杂一些,那么我们假定你建立了这样的规则,所有的使用于NiftyContainer 的多态类型必须定义一个Clone虚拟函数(virtual function)。那么你就可以像这样来克隆对象:
T* pNewObj = pSomeObj->Clone();
因为你的容器(container)必须能够接受这两种类型,所以你必须实现两种克隆算法并在编译时刻选择适当的一个。那么不管通过NiftyContainer的布尔(非类型,non-type)模板参数传递的类型是不是多态的,你都要和它交互,而且还要依赖程序员给它传递的是正确的标识。
template <typename T, bool isPolymorphicWithClone>
class NiftyContainer
{
...
};
NiftyContainer<Widget, true> widgetBag;
NiftyContainer<double, false> numberBag;
如果你存储在NiftyContainer里的类型不是多态的,那么你就可以对NiftyContainer的许多成员函数进行优化处理,因为可以借助于常量的对象大小(constant object size)和值语义(value semantics)。在所有的这些成员函数中,你需要选择一个算法,或是另外一个依赖于模板参数isPolymorphic的算法。
乍一看,似乎只用一个if语句就可以了。
template <typename T, bool isPolymorphic>
class NiftyContainer
{
...
void DoSomething(T* pObj)
{
if (isPolymorphic)
{
... polymorphic algorithm ...
}
else
{
... non-polymorphic algorithm ...
}
}
};
问题是编译器是不会让你摆脱这些代码的。例如,如果多态算法使用了pObj->Clone,那么NiftyContainer::DoSomething就不会那些任何一个没有定义Clone成员函数的类型而编译。的确,看起来在编译时刻要执行哪一个if语句分支是很明显的,但是这不关编译器的事,编译器仍然坚持不懈地尽心尽职地编译这两个分支,即使优化器最终会消除这些废弃代码(dead code)。如果你试图调用NiftyContainer<int, false>的DoSomething函数的话,编译器就会停留在pObj->Clone的调用之处,这是怎么回事?
等等,问题还多着呢。如果T是一个多态类型,那么代码将又一次不能通过编译了。如果T将它的copy constructor设为private和protected,禁止外部对其访问——作为一个行为良好的多态类,应该如此。那么,如果非多态的代码分支要做new T(*pObj),则代码不能编译通过。
如果编译器不为编译废弃代码费神那多好啊,但无望的期望不是解决之道,那么怎样才是一个满意的解决方案呢?
已经证实,有许多的解决办法。Int2Type就提供了一个非常精巧的解决方案。对应于isPolymorphic的值为true和false,Int2Type可以将特定的布尔值isPolymorphic转化为两个不同的类型。那么你就可以通过简单的重载(overloading)来使用Int2Type<isPolymorphic>了,搞定!
“整型类型化”(integral typifying)风格的原型(incarnation)如下所示:
template <typename T, bool isPolymorphic>
class NiftyContainer
{
private:
void DoSomething(T* pObj, Int2Type<true>)
{
... polymorphic algorithm ...
}
void DoSomething(T* pObj, Int2Type<false>)
{
... non-polymorphic algorithm ...
}
public:
void DoSomething(T* pObj)
{
DoSomething(pObj, Int2Type<isPolymorphic>());
}
};
这个代码简单扼要,DoSomething调用重载了的私有成员函数,根据isPolymorphic的值,两个私有重载函数之一被调用,从而完成了分支。这里,类型Int2Type<isPolymorphic>的虚拟临时变量没有被用到,它只是为传递类型信息之用。
不要太快了,天行者!
看到上面的方法,你可能认为还有更为巧妙的解决之道,可以使用比如template specialization这样的技巧。为什么必须用虚拟的临时变量,一定还有更好的方式。但是,令人惊奇的是,在简单性、通用性和效率上,Int2Type是很难打败的。
一个可能的尝试是,根据任意的T及isPolymorphic的两个可能的值,对NiftyContainer::DoSomething作特殊处理。这不就是partial template specialization的拿手戏吗?
template <typename T>
void NiftyContainer<T, true>::DoSomething(T* pObj)
{
... polymorphic algorithm ...
}
template <typename T>
void NiftyContainer<T, false>::DoSomething(T* pObj)
{
... non-polymorphic algorithm ...
}
看上去很美,可是啊呀,不好,它是不合法的。没有这样的对一个类模板的成员函数进行partial specialization的方式,你可以对整个NiftyContainer作partial specialization:
template <typename T>
class NiftyContainer<T, false>
{
... non-polymorphic NiftyContainer ...
};
你也可以对整个DoSomething作specialization:
template <>
void NiftyContainer<int, false>::DoSomething(int* pObj)
{
... non-polymorphic algorithm ...
}
但奇怪的是,在[1]之间,你不能做任何事。
另一个办法可能是引入traits技术[2],并在NiftyContainer的外部来实现DoSomething(在traits类中),但把DoSomething分开来实现显得有些笨拙了。
第三个办法仍然试图用traits技术,但把实现都放在一起,这就要在NiftyContainer里面把traits定义为私有的内部类。总之,这是可以的,但在你设法实现的时候,你就会认识到基于Int2Type的风格有多好。而且这种风格最好的地方可能就在于:在实际应用中,你可以把这个小小的Int2Type模板放在库中,并把它的预期使用记录在案。
类型到类型的映射
考虑下面这个函数:
template <class T, class U>
T* Create(const U& arg)
{
return new T(arg);
}
Create通过传递一个参数给T的构造函数(constructor)而产生了一个新的对象。
现在假设在你的应用中用这么一个规则:类型Widget的对象是遗留下来的代码,在构造时必须要带两个参数,第二个参数是一个像-1这样的固定值。在所有派生自Widget的类中你不会碰到什么问题。
你要怎么对Create作特殊化处理,才能让它在处理Widget时,不同于所有的其它类型呢?你是不可以对函数作partial specialization的,也就是说,你不能像这样做:
// Illegal code - don't try this at home
template <class U>
Widget* Create<Widget, U>(const U& arg)
{
return new Widget(arg, -1);
}
由于缺乏函数的partial specialization,我们所拥有的唯一工具,还是重载。可以传递一个类型T的虚拟对象,并重载。
// An implementation of Create relying on overloading
template <class T, class U>
T* Create(const U&