文章目录
- 一、动态多态(dynamic polymorphism)
- 二、静态多态
- 三、静态多态VS动态多态
- 1.术语
- 2.优点和缺点
- 3.结合两种多态形式:CRTP
- 四、使用concepts
- 五、新形势的设计模式
- 六、泛型编程
- 七、总结
一、动态多态(dynamic polymorphism)
动态多态:继承和虚函数实现;
静态多态:模板也允许我们用单个统一符号将不同的特定行为关联起来, 不过该关联主要发生在编译期间;
#include "coord.hpp"
// common abstract base class GeoObj for geometric objects
class GeoObj
{
public:
// draw geometric object:
virtual void draw() const = 0;
// return center of gravity of geometric object:
virtual Coord center_of_gravity() const = 0;
virtual ~GeoObj() = default;
};
// concrete geometric object class Circle
// - derived from GeoObj
class Circle : public GeoObj
{
public:
virtual void draw() const override;
virtual Coord center_of_gravity() const override;
};
// concrete geometric object class Line
// - derived from GeoObj
class Line : public GeoObj
{
public:
virtual void draw() const override;
virtual Coord center_of_gravity() const override;
};
// draw any GeoObj
void myDraw(GeoObj const &obj)
{
obj.draw(); // call draw() according to type of object
}
// compute distance of center of gravity between two GeoObjs
Coord distance(GeoObj const &x1, GeoObj const &x2)
{
Coord c = x1.center_of_gravity() - x2.center_of_gravity();
return c.abs(); // return coordinates as absolute values
}
// draw heterogeneous collection of GeoObjs,处理异质集合中不同类型的对象
void drawElems(std::vector<GeoObj *> const &elems)
{
for (std::size_type i = 0; i < elems.size(); ++i)
{
elems[i]->draw(); // call draw() according to type of element
}
}
/*在通过基类的指针或者引用调用一个虚函数的时候, 所调用的函数将是指针或者引用所指对象的真正类型中的相应函数*/
int main()
{
Line l;
Circle c, c1, c2;
myDraw(l); // myDraw(GeoObj&) => Line::draw()
myDraw(c); // myDraw(GeoObj&) => Circle::draw()
distance(c1, c2); // distance(GeoObj&,GeoObj&)
distance(l, c); // distance(GeoObj&,GeoObj&)
std::vector<GeoObj *> coll; // heterogeneous collection
coll.push_back(&l); // insert line
coll.push_back(&c); // insert circle
drawElems(coll); // draw different kinds of GeoObjs
}
二、静态多态
模板也可以被用来实现多态。 不同的是, 它们不依赖于对基类中公共行为的分解。 取而代之的是, 这一“共性( commonality) ” 隐式地要求不同的“形状( shapes) ” 必须支持使用了相同语法的操作。
- eg:相关函数的名字必须相同
- 比较 myDraw()的两种实现, 可以发现其主要的区别是将 GeoObj 用作模板参数而不是公共基类。
- 但是, 在表象之下还有很多区别:使用动态多态的话, 在运行期间只有一个 myDraw()函数, 但是在使用模板的情况下, 却会有多种不同的函数, 例如 myDraw<Line>()和myDraw<Circle>()
比如, 上一节中的 myDraw():
void myDraw (GeoObj const& obj) // GeoObj is abstract base
class
{
obj.draw();
}
//也可以被实现成下面这样:
template<typename GeoObj>
void myDraw (GeoObj const& obj) // GeoObj is template parameter
{
obj.draw();
}
- 动态多态的eg改造成静态多态
#include "coord.hpp"
#include <vector>
// concrete geometric object class Circle
// - not derived from any class
class Circle
{
public:
void draw() const;
Coord center_of_gravity() const;
};
// concrete geometric object class Line
// - not derived from any class
class Line
{
public:
void draw() const;
Coord center_of_gravity() const;
}
// draw any GeoObj
template <typename GeoObj>
void myDraw(GeoObj const &obj)
{
obj.draw(); // call draw() according to type of object
}
// compute distance of center of gravity between two GeoObjs
template <typename GeoObj1, typename GeoObj2>
Coord distance(GeoObj1 const &x1, GeoObj2 const &x2)
{
Coord c = x1.center_of_gravity() - x2.center_of_gravity();
return c.abs(); // return coordinates as absolute values
}
// draw homogeneous collection of GeoObjs
template <typename GeoObj>
void drawElems(std::vector<GeoObj> const &elems)
{
for (unsigned i = 0; i < elems.size(); ++i)
{
elems[i].draw(); // call draw() according to type of element
}
}
int main()
{
Line l;
Circle c, c1, c2;
myDraw(l); // myDraw<Line>(GeoObj&) => Line::draw()
myDraw(c); // myDraw<Circle>(GeoObj&) =>Circle::draw()
distance(c1, c2); // distance<Circle,Circle>(GeoObj1 &, GeoObj2 &)
/*
引入了两个模板参数, GeoObj1 和 GeoObj2, 来支持不同类型的集合对象之间的距离计算:
*/
distance(l, c); // distance<Line,Circle>(GeoObj1&,GeoObj2&)
/*我们将不再能够透明地处理异质容器。 这也正是 static 多态中的 static部分带来的限制: 所有的类型必须在编译期可知。
不过, 我们可以很容易的为不同的集合对象类型引入不同的集合。 这样就不再要求集合的元素必须是指针类型,
这对程序性能和类型安全都会有帮助*/
// std::vector<GeoObj*> coll; //ERROR: no heterogeneous collection possible
std::vector<Line> coll; // OK: homogeneous collection possible
coll.push_back(l); // insert line
drawElems(coll); // draw all lines
}
三、静态多态VS动态多态
1.术语
Static 和 dynamic 多态提供了对不同 C++编程术语的支持:
- 通过继承实现的多态是有界的(bounded) 和动态的(dynamic) :
有界的意思是, 在设计公共基类的时候, 参与到多态行为中的类型的相关接口就已经确定(该概念的其它一些术语是侵入的(invasive 和 intrusive) ) 。
动态的意思是, 接口的绑定是在运行期间执行的。
- 通过模板实现的多态是无界的(unbounded) 和静态的(static) :
无界的意思是, 参与到多态行为中的类型的相关接口是不可预先确定的(该概念的其它一些术语是非侵入的(noninvasive 和 nonintrusive) )
静态的意思是, 接口的绑定是在编译期间执行的
动态多态:有界动态多态;
静态多态:无界静态多态;
2.优点和缺点
C++中的动态多态有如下优点:
可以很优雅的处理异质集合。
可执行文件的大小可能会比较小(因为它只需要一个多态函数, 不像静态多态那样, 需要为不同的类型进行各自的实例化) 。
代码可以被完整的编译; 因此没有必须要被公开的代码(在发布模板库时通常需要发布模板的源代码实现)
C++中 static 多态的优点:
内置类型的集合可以被很容易的实现。 更通俗地说, 接口的公共性不需要通过公共基类实现。
产生的代码可能会更快(因为不需要通过指针进行重定向, 先验的(priori) 非虚函数通常也更容易被 inline) 。
即使某个具体类型只提供了部分的接口, 也可以用于静态多态, 只要不会用到那些没有被实现的接口即可。
通常认为静态多态要比动态多态更类型安全(type safe) , 因为其所有的绑定都在编译期间进行了检查。
- 例如, 几乎不用担心将一个通过模板实例化得到的、 类型不正确的对象插入到一个已有容器中(编译期间会报错) 。 但是, 对于一个存储了指向公共基类的指针的容器,其所存储的指针却有可能指向一个不同类型的对象。
3.结合两种多态形式:CRTP
为了能够操作集合对象的异质集合, 你可以从一个公共基类中派生出不同的集合对象。 而且, 你依然可以使用模板为某种形式的集合对象书写代码。
一个成员函数的虚拟性是如何被参数化的, 以及我们是如何通过 curiously recurring template pattern(CRTP) 为 static多态提供额外的灵活性的。
四、使用concepts
模板的静态多态的一个巨大问题:
-
接口的绑定是通过实例化相应的模板执行的。 也就是说没有可供编程的公共接口或者公共 class。 取而代之的是, 如果所有实例化的代码都是有效的, 那么对模板的任何使用也都是有效的。 否则, 就会导致难以理解的错误信息, 或者是产生了有效的代码却导致了意料之外的行为。
-
基于这一原因, C++语言的设计者们一直在致力于实现一种能够为模板参数显式地提供(或者是检查) 接口的能力。 在 C++中这一接口被称为 concept。 它代表了为了能够成功的实例化模板, 模板参数必须要满足的一组约束条件。
-
Concept 可以被理解成静态多态的一类“接口”
template <typename T>
concept GeoObj = requires(T x) {
{ x.draw() } -> void;
{ x.center_of_gravity()} -> Coord;
};
#include <vector>
//clang-format off code
/*
用关键字 concept 定义了一个 GeoObj concept, 它要求一个类型要有可被调用
的成员函数 draw()和 center_of_gravity(), 同时也对它们的返回类型做了限制。
*/
template <typename T>
concept GeoObj = requires(T x) {
{
x.draw()
} -> void;
{
x.center_of_gravity()
} -> Coord;
};
//clang-format on code
// 使用 requires 子句要求模板参数满足GeoObj concept
// draw any GeoObj
template <typename T>
requires GeoObj<T>
void myDraw(T const &obj)
{
obj.draw(); // call draw() according to type of object
}
// compute distance of center of gravity between two GeoObjs
template <typename T1, typename T2>
requires GeoObj<T1> && GeoObj<T2>
Coord distance(T1 const &x1, T2 const &x2)
{
Coord c = x1.center_of_gravity() - x2.center_of_gravity();
return c.abs(); // return coordinates as absolute values
}
// draw homogeneous collection of GeoObjs
template <typename T>
requires GeoObj<T>
void drawElems(std::vector<T> const &elems)
{
for (std::size_type i = 0; i < elems.size(); ++i)
{
elems[i].draw(); // call draw() according to type of element
}
}
// 对于那些可以参与到静态多态行为中的类型, 该方法依然是非侵入的:
// concrete geometric object class Circle
// - not derived from any class or implementing any interface
class Circle
{
public:
void draw() const;
Coord center_of_gravity() const;
};
五、新形势的设计模式
桥接模式:在不同的接口实现之间做切换。
动态多态的桥接模式:
- 根据[DesignPatternsGoF], 桥接模式通常是通过使用一个接口类实现的, 在这个接口类中包
含了一个指向具体实现的指针, 然后通过该指针委派所有的函数调用(参见图 18.3) 。
静态多态的桥接模式:
- 但是, 如果具体实现的类型在编译期间可知, 我们也可以利用模板实现桥接模式
- 这样做会更类型安全(一部分原因是避免了指针转换) , 而且性能也会更好
六、泛型编程
STL 是一个框架, 它提供了许多有用的操作(称为算法) , 用于对象集合(称为容器) 的一些线性数据结构。 算法和容器都是模板。
但是, 关键的是算法本身并不是容器的成员函数。 算法被以一种泛型的方式实现,因此它们可以用于任意的容器类型(以及线性的元素集合)。
为了实现这一目的, STL的设计者们找到了一种可以用于任意线性集合、 称之为迭代器(iterators) 抽象概念。
- 从本质上来说, 容器操作中针对于集合的某些方面已经被分解到迭代器的功能中。
- eg:这样我们就可以在不知道元素的具体存储方式的情况下, 实现一种求取序列中元素最大值
的方法
template <typename Iterator>
Iterator max_element(Iterator beg, // refers to start of collection
Iterator end) // refers to end of collection
{
// use only certain Iterator operations to traverse all elements
// of the collection to find the element with the maximum value
// and return its position as Iterator
…
}
/*
容器本身只要提供一个能够遍历序列中数值的迭代器类型, 以及一些能够创建这些迭代器的成员函数
*/
namespace std
{
template <typename T, …>
class vector
{
public:
using const_iterator = …; // implementation-specific iterator
… // type for constantvectors
const_iterator begin() const; // iterator for start of collection
const_iterator end() const; // iterator for end of collection
…
};
template <typename T, …>
class list
{
public:
using const_iterator = …; // implementation-specific iterator
… // type for constant lists
const_iterator begin() const; // iterator for start of collection
const_iterator end() const; // iterator for end of collection
…
};
}
template <typename T>
void printMax(T const &coll)
{
// compute position of maximum value
auto pos = std::max_element(coll.begin(), coll.end());
// print value of maximum element of coll (if any):
if (pos != coll.end())
{
std::cout << *pos << ’ \n’;
}
else
{
std::cout << "empty" << ’ \n’;
}
}
int main()
{
std::vector<MyClass> c1;
std::list<MyClass> c2;
…
printMax(c1);
printMax(c2);
}
通过用这些迭代器来参数化其操作, STL 避免了相关操作在定义上的爆炸式增长。
我们并没有为每一种容器都把每一种操作定义一遍, 而是只为一个算法进行一次定义, 然后将其用于所有的容器。
泛型的关键是迭代器, 它由容器提供并被算法使用。 这样之所以可行, 是因为
迭代器提供了特定的、 可以被算法使用的接口。 这些接口通常被称为 concept, 它代表了为
了融入该框架, 模板必须满足的一组限制条件。 此外, 该概念还可用于其它一些操作和数据
结构
泛型编程之所以实用, 正是因为它依赖于静态多态, 这样就可以在编译期间就决定具体的接口。
另一方面, 需要在编译期间解析出接口的这一要求, 又催生出了一些与面向对象设计原
则(object oriented principles) 不同的新原则。