今天看了JAVA的泛型部分,本来有些期待这部分内容的,看了之后却隐隐有些失望,感觉最直接的好处就是不用自己写转型代码.个人不喜欢JAVA的擦除式实现方式,这种实现方式根本展现不出泛型的威力.感觉这种实现方式甚至不如只在C++中简单的运用模板时代.不过这对JAVA本身来说未必不是一件好事.JAVA主要倾向于OO,如果JAVA中的泛型过于强大,那么JAVA就有可能分裂出两种范式出来,这并非好事.这种擦除式实现,不会动摇OO在JAVA中的绝对地位,让JAVA可以保持一个统一的人格:).
这些麻烦的事还是交给库的实现者去做吧.
当然写这篇文章不是为了批评JAVA的泛型,是为了说明在C++模板中如何加入约束的.
在JAVA中可以写这样的代码.
public class GPTest
{
public static void main(String[] argv)
{
A a = new A();
GP<A> gpa= new GP<A>(a);
System.out.println(gpa);
B b = new B();
GP<B> gpb = new GP<B>(b);
System.out.println(b);
/*
C c = new C();
GP<C> gpc = new GP<C>(c);
System.out.println(gpc);
*/
}
}
class A
{
public String toString()
{
return "A";
}
}
class B extends A
{
public String toString()
{
return "B";
}
}
class C
{
public String toString()
{
return "C";
}
}
class GP<T extends A>
{
GP(T t)
{
this.t = t;
}
public String toString()
{
return t.toString();
}
private T t;
}
上面被注释掉的部分不能编译通过,因为C不是A的子类, class GP<T extends A>
要求T必须是A的子类,在擦除的时候T就被擦除了A类型.个人认为这是JAVA泛型中一个不错的地方,我不是指擦除不错,指强制T为A的子类这一点不错.
可能有的C++的fans会说这种语法丑陋,不够泛型,不能做到最大的复用.
个人一直相信过犹不及.首先贴一段关于C++ 0x的说明(出自 C++0x概览)
----------------------------------------------------------------------------------------------------------------------------------
为了在STL的框架内做这件事,我们必须针对容器和迭代器对sort进行重载。例如:
template<Container C> // 使用<对容器排序
void sort(C& c);
template<Container C, Predicate Cmp> // 使用Cmp对容器排序
where Can_call_with<Cmp,typename C::value_type>
void sort(C& c, Cmp less);
template<Random_access_iterator Ran> // 使用<对序列排序
void sort(Ran first, Ran last);
template<Random_access_iterator Ran, Predicate Cmp> // 使用Cmp对序列排序
where Can_call_with<Cmp,typename Ran::value_type>
void sort(Ran first, Ran last, Cmp less);
这里演示了C++0x目前提案中最有意义的扩展部分(也是有可能被接受的部分):concepts。基本上,一个concept就是一个type的type,它指定了一个type所要求的属性。在这个例子中,concept Container用于指定前两个版本的sort需要一个满足标准库容器要求的实参,where子句用于指定模板实参之间所要求的关系,即判断式(predicate)可以被应用在容器的元素类型上。有了concepts,我们就可以提供比目前好得多的错误消息,并区分带有相同数目实参的模板,例如:
sort(v, Case_insensitive_less()); // 容器与判断式
和
sort(v.begin(), v.end()); // 两个随机访问迭代器
在concepts的设计中存在的最大的困难是维持模板的灵活性,因此我们不要求模板实参适合于类层次结构或要求所有操作都能够通过虚函数进行访问(就象Java和C#的泛型所做的那样)。在这些语言的“泛型”中,实参的类型必须是派生自泛型定义中指定的接口(C++中类似于接口的是抽象类)。这意味着所有的泛型实参都必须适合于某个类层次结构。这将要求开发人员在设计时要进行一些不合理的预设,从而为他们强加了一些不必要的约束。例如,如果你编写了一个泛型类,而我又定义了一个类,只有在我知道你指定的接口、并将我的类从该接口派生的情况下,人们才可以将我的类用作这个泛型类的实参。这种限制太过严格。
当然对于这种问题总有解决办法,但那会使代码变得复杂化。另一个问题是我们不能直接在泛型中使用内建类型。因为内建类型(例如int)并不是类,也就没有泛型中指定接口所要求的函数——这时候你必须为这些内建类型做一个包装器类,然后通过指针间接地访问它们。另外,在泛型上的典型操作会被实现为一个虚函数调用。那样的代价可能相当高(相对于仅仅使用简单的内建操作来说,比如+或者<)。以这种方式来实现的泛型,只不过是抽象类的“语法糖”。
有了concepts后,模板将保持它们的灵活性和性能。在委员会可以接受一个具体的concept设计之前,仍然有很多工作要做。然而,由于承诺显著更好的类型检查、更好的错误信息和更好的表达力,concepts将 会成为一个可能性极大的扩展。它允许我们从目前的标准容器、迭代器以及算法开始就设计出更好的库接口。
------------------------------------------------------------------------------------------------------------------------------------------
Concept相当值得期待,上面讲得JAVA和C#中的concept check检查过于严格了,或许吧.
不过有时候这种限制可能是必须的.比如说我不希望上面的类C也用到类GP的行为,为了减少误用,就应该进行这种检查.当然如果规定模板参数必须为一个类的子类,那么用多态就行了,用不着模板.但向上面说的那样多态有效率上的损失.(或许这点损失一点都不重要 : ))
下面是一个编译期的类JAVA concept check,没有使用虚函数,不会带来性能上的损失: )
template<typename D, typename B>
struct must_have_base
{
~must_have_base()
{
void(*p)(D*,B*) = constraints;
}
private:
static void constraints(D* pd, B* pb)
{
pb = pd;
}
};
class A
{
public:
string toString()
{
return "A";
}
};
class B
{
public:
string toString()
{
return "B";
}
};
template<typename T>
class GP
{
public:
string toString()
{
return t.toString();
}
private:
T t;
private:
must_have_base<T, A> must_have_base_A_;
must_have_base<T, B> must_hava_base_B_;
};
class C:public A,public B
{
public:
string toString()
{
return "C";
}
};
class D
{
public:
string toString()
{
return "D";
}
};
int main(int argc, char* argv[])
{
GP<C> gpc;
cout<<gpc.toString()<<endl;
//GP<D> gpd;
//cout<<gpd.toString()<<endl;
}
上面被注释掉的那两句同样不能通过编译.
这个检查规定GP的模板参数必须为A和B的子类, must_have_base这个模板类来自<<Imperfect C++>>
一书,它强制规定D必须为B的派生类,否则在编译期就会出错.个人非常喜欢”约束”这个概念,订立编译期的契约可以省去以后很多麻烦,不过使用这种方法编译器错误信息非常糟糕.
当然约束并不只于此,在<<Imperfect C++>>一书中还提到很多其它约束,在模板中结合约束,个人比较喜欢: ).