谈泛型(GP)之前, 先谈一下面向对象(OO), OO强调世界是由对象组成的,对象是由方法和属性组成的(个人感觉还应该加上事件),而对象之间又有继承(is-a)和组合等 关系。OO很符合我们认识世界的直觉,它以封装,继承和多态为特性,我们在现实工作中又总结出来了OO的5大设计原则和23种设计模式。总之,OO基本上已经可以很好的解决我们现实生活中的所有问题。
那么既然OO已经可以很好的解决我们的问题了,为什么还要有GP?
我们先来看一下OO的缺点:
OO的多态是通过继承自同一接口来实现的, 修改接口会导致所有派生类的修改,耦合比较紧密。
OO的多态是运行时的, 性能比较低。
OO的多态通过抽象接口实现,使用不安全,在不支持反射的语言(比如C++)中会丧失类型检测。
用OO设计时,你只能通过抽象接口来处理不同的事物。比如你要让Cat和Dog同时跑动,你可能会抽象出一个IAnimal接口,内部有一个Run的方法,然后让Cat和Dog分别继承。但是如果你想让猫(Cat)和玩具猫(ToyCat)支持跑动,这时你抽象出IAnimal就不合适了, 你可能会抽象出ICatProperty。可以看到OO的这种继承体系非常笨重,到后面可能会导致非常深的继承层次(比如MFC),代码扩展和维护都很艰难。
上面OO的这些缺点在GP没有被发现之前都不是缺点,因为我们没有更好的解决方案,但是人们逐渐发现了GP,GP最初是在使用容器(Container)时被发现的,container<T>可以放任意类型的元素,根据元素类型,我们可以可以生成任意类型的container. 这些技术后来发展成泛型编程和模板元编程。
那么什么是GP?
我的理解是 GP是基于concept的程序设计,我们通过concept来定义对象之间的关系。
什么是concept?
按我的理解,concept就是我们自己定义的概念和规则。
比如说任何对象,只要可赋值,他就符合Assignable的concept:
又比如任何对象,只要可比较, 他就符合Comparable的concept:
再看一个例子:
GP的concept可以是任何我们能想到的规则,比如包含某个成员变量,或是定义某个类型, 比如下面代码:
甚至GP的concept可以是指明基类和派生类, 比如下面代码:
这些concept在不同语言中定义不一样,在一些语言中(比如C#)我们可以通过定义来进行显式约束,表明我们某个concept要满足哪些条件;在有些语言中(比如C++)则没有这显式约束定义,我们只能通过我们编程时自己的逻辑来保证,当然如果某个方法要求他的对象满足某个concept而你的对象没有满足 ,编译器也是不会让你通过的。
另外GP的concept之间本身也有某种关系, 某个concept可能继承与另外一个concept,或者说某个concept是另外一个concept的强化,也就是满足某个concept的对象肯定也同时满足另外一个concept. 比如concept A包含Run方法, concept B包含Run和Eat方法,则concept B是concept A的强化,满足concept B的对象肯定同时满足concept A。
有些人说GP的抽象能力高于OO,这个观点我并不认同,我感觉只是他们的抽象方式不一样,OO是基于接口, 而GP是基于concept。OO的基于接口的抽象,在源代码和最终运行时都能体现,源代码中是接口,运行时是虚表,所以他们是一致的, 符合普通人的思维习惯。GP基于concept的抽象, 主要体现在源代码中 ,只是你用来告诉编译器你的思维方式, 在运行时他可能是一个完全不同的世界,所以比较难理解。
如果说OO的设计是抽象出接口, GP的设计就是抽象出concept, 满足某个concept的class是一个template class(如template<typename T> class vector), 而template class的又可以实例化成某个特定的class(如vector<int>)。 所以GP可以大大减少我们源代码的数量,但是他本身不能减小我们最终编译的可执行文件的大小,相反如果模板类过大,反而会造成代码膨胀,而OO的继承则没有这个问题(可参考C++模板会使代码膨胀吗)。
对于在实际开发中主要用OO还是用GP的问题, 可能应人和公司而异,最好的方式当然是OO和GP的灵活结合。有人觉得GP的代码不好理解,那是因为你没有理解他的concept,比如看了《泛型编程与STL》,你就会发现自己也可以扩展STL了。当然,因为C++对concept没有约束定义机制, 而concept这个东西注释又不好描述,所以大部分时候我们只能通过源代码来推理,这在一定程度上也造成了GP代码难读懂这种说法的流行。
不知道大家在实际开发中GP用的多不多?
那么既然OO已经可以很好的解决我们的问题了,为什么还要有GP?
我们先来看一下OO的缺点:
OO的多态是通过继承自同一接口来实现的, 修改接口会导致所有派生类的修改,耦合比较紧密。
OO的多态是运行时的, 性能比较低。
OO的多态通过抽象接口实现,使用不安全,在不支持反射的语言(比如C++)中会丧失类型检测。
用OO设计时,你只能通过抽象接口来处理不同的事物。比如你要让Cat和Dog同时跑动,你可能会抽象出一个IAnimal接口,内部有一个Run的方法,然后让Cat和Dog分别继承。但是如果你想让猫(Cat)和玩具猫(ToyCat)支持跑动,这时你抽象出IAnimal就不合适了, 你可能会抽象出ICatProperty。可以看到OO的这种继承体系非常笨重,到后面可能会导致非常深的继承层次(比如MFC),代码扩展和维护都很艰难。
上面OO的这些缺点在GP没有被发现之前都不是缺点,因为我们没有更好的解决方案,但是人们逐渐发现了GP,GP最初是在使用容器(Container)时被发现的,container<T>可以放任意类型的元素,根据元素类型,我们可以可以生成任意类型的container. 这些技术后来发展成泛型编程和模板元编程。
那么什么是GP?
我的理解是 GP是基于concept的程序设计,我们通过concept来定义对象之间的关系。
什么是concept?
按我的理解,concept就是我们自己定义的概念和规则。
比如说任何对象,只要可赋值,他就符合Assignable的concept:
class A {};
class B {};
void test()
{
A a1; A a2; a1 = a2;
B b1; B b2; b1 = b2;
}
上面的A和B就都符合Assignable的concept.class B {};
void test()
{
A a1; A a2; a1 = a2;
B b1; B b2; b1 = b2;
}
又比如任何对象,只要可比较, 他就符合Comparable的concept:
class C
{
public:
bool operator < (const C& c);
bool operator > (const C& c);
bool operator == (const C& c);
bool operator != (const C& c);
bool operator <= (const C& c);
bool operator >= (const C& c);
};
上面的对象C,他就符合他就符合Comparable的concept。{
public:
bool operator < (const C& c);
bool operator > (const C& c);
bool operator == (const C& c);
bool operator != (const C& c);
bool operator <= (const C& c);
bool operator >= (const C& c);
};
再看一个例子:
class Dog
{
public:
void run();
};
class Cat
{
public:
void run();
};
template<typename T>
void run(T& t)
{
t.run();
}
上面的Cat和Dog就都符合拥有成员函数Run这个concept.{
public:
void run();
};
class Cat
{
public:
void run();
};
template<typename T>
void run(T& t)
{
t.run();
}
GP的concept可以是任何我们能想到的规则,比如包含某个成员变量,或是定义某个类型, 比如下面代码:
class myTraits
{
public:
typedef int type;
type value;
};
template<typename T>
typename T::type GetValue(T& t)
{
return t.value;
}
{
public:
typedef int type;
type value;
};
template<typename T>
typename T::type GetValue(T& t)
{
return t.value;
}
甚至GP的concept可以是指明基类和派生类, 比如下面代码:
template<typename TDrived, typename TBase>
class CMyImpl: public TBase
{
public:
void work()
{
TDrived* p = static_cast<TDrived*>(this);
p->Hello();
}
};
class CMyBase {};
class CMyClass: public CMyImpl<CMyClass, CMyBase>
{
public:
void Hello() {}
};
ATL中大量应用这种concept来实现代码重用和模拟虚函数。class CMyImpl: public TBase
{
public:
void work()
{
TDrived* p = static_cast<TDrived*>(this);
p->Hello();
}
};
class CMyBase {};
class CMyClass: public CMyImpl<CMyClass, CMyBase>
{
public:
void Hello() {}
};
这些concept在不同语言中定义不一样,在一些语言中(比如C#)我们可以通过定义来进行显式约束,表明我们某个concept要满足哪些条件;在有些语言中(比如C++)则没有这显式约束定义,我们只能通过我们编程时自己的逻辑来保证,当然如果某个方法要求他的对象满足某个concept而你的对象没有满足 ,编译器也是不会让你通过的。
另外GP的concept之间本身也有某种关系, 某个concept可能继承与另外一个concept,或者说某个concept是另外一个concept的强化,也就是满足某个concept的对象肯定也同时满足另外一个concept. 比如concept A包含Run方法, concept B包含Run和Eat方法,则concept B是concept A的强化,满足concept B的对象肯定同时满足concept A。
我们可以看到GP的这种基于concept的设计方式,大大降低了对象之间的耦合性,我们不再要求象OO那样抽象出共同的接口来让大家继承;任何两个看似没有关系的对象, 只要他们满足某个concept的约束,他们就能当作模板参数传给GP代码(模板类或是模板函数)。另外,我们可以看到GP的这些类型检测是在编译时就完成的,他的多态是在编译时就确定的静多态, 效率大大高于OO的动多态。
有些人说GP的抽象能力高于OO,这个观点我并不认同,我感觉只是他们的抽象方式不一样,OO是基于接口, 而GP是基于concept。OO的基于接口的抽象,在源代码和最终运行时都能体现,源代码中是接口,运行时是虚表,所以他们是一致的, 符合普通人的思维习惯。GP基于concept的抽象, 主要体现在源代码中 ,只是你用来告诉编译器你的思维方式, 在运行时他可能是一个完全不同的世界,所以比较难理解。
如果说OO的设计是抽象出接口, GP的设计就是抽象出concept, 满足某个concept的class是一个template class(如template<typename T> class vector), 而template class的又可以实例化成某个特定的class(如vector<int>)。 所以GP可以大大减少我们源代码的数量,但是他本身不能减小我们最终编译的可执行文件的大小,相反如果模板类过大,反而会造成代码膨胀,而OO的继承则没有这个问题(可参考C++模板会使代码膨胀吗)。
对于在实际开发中主要用OO还是用GP的问题, 可能应人和公司而异,最好的方式当然是OO和GP的灵活结合。有人觉得GP的代码不好理解,那是因为你没有理解他的concept,比如看了《泛型编程与STL》,你就会发现自己也可以扩展STL了。当然,因为C++对concept没有约束定义机制, 而concept这个东西注释又不好描述,所以大部分时候我们只能通过源代码来推理,这在一定程度上也造成了GP代码难读懂这种说法的流行。
不知道大家在实际开发中GP用的多不多?