C++模板(第二版)笔记之第十八章:模板的多态性
文章目录
一、动态多态(dynamicpolymorphism)
二、静态多态
三、静态多态VS动态多态
1.术语
2.优缺点
三、结合两种多态形式:CRTP
四、使用concepts
五、新形势的设计模式
六、泛型编程
七、总结
一、动态多态(dynamicpolymorphism)
动态多态:实现继承和虚拟函数;
静态多态:模板还允许我们使用单个统一符号来关联不同的特定行为,但这种关联主要发生在编译过程中;
#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动态多态VS
1.术语
Static和dynamic多态化为不同的C++编程术语提供了支持:
通过继承实现的多态性是有界的。(bounded)和动态的(dynamic):
边界意味着,在设计公共基类时,已经确定了参与多种行为的类型的相关界面(这个概念的其他术语都是入侵的(invasive和intrusive))。
动态性意味着,接口的绑定是在运行过程中进行的。
多态通过模板实现是无界的。(unbounded)和静态的(static):
无界意味着参与多种行为的相关界面不能提前确定(这个概念的其他术语是非侵入性的(noninvasive和nonintrusive))
静止意味着,在编译过程中,接口的绑定是执行的。
动态多态:有界动态多态;
静态多态:无界静态多态;
优点和缺点
C++动态多态中有以下优点:
处理异质集合可以非常优雅。
可执行文件的大小可能相对较小(因为它只需要一个多态函数,不像静态多态函数,它需要为不同类型的各自实例化)。
代码可完全编译;所以不需要公开代码(在发布模板库时,通常需要发布模板的源代码来实现)
C++多态中static的优点:
内置类型的集合很容易实现。通俗地说,接口的公共性不需要通过公共基类来实现。
代码的产生可能会更快(因为没有必要通过指针重定向,先验的代码(priori)通常情况下,非虚函数也更容易被inline)。
即便某一特定类型只提供部分接口,只要不使用未实现的接口,也可用于静态多态接口。
一般认为静态多态比动态多态多态更安全。(typesafe),由于它的所有绑定都是在编译过程中检查的。
比如几乎不用担心在现有容器中插入一个通过模板实例获得的类型不正确的对象(编译过程中会出错)。然而,对于一个存储指向公共基类的指针的容器来说,它存储的指针可能指向不同类型的对象。
结合两种多态形式:CRTP
为了操作集合对象的异质集合,您可以从一个公共基类中衍生出不同的集合对象。此外,您仍然可以使用模板作为某种形式的集合对象来编写代码。
成员函数的虚拟性是如何参数化的,我们是如何通过curiouslyrecurringtemplatternter的?(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,它代表
为了整合框架,模板必须满足一组限制。此外,该概念还可用于其他操作和数据
结构
泛型编程之所以实用,是因为它依赖于静态多态,因此在编译过程中可以确定具体的接口。
另一方面,在编译过程中需要分析界面的要求,并产生了一些与面向对象设计的原始要求
则(objectorientedprinciples)不同的新原则。