C++作为一种同时支持面向对象编程和泛型编程的语言,其面向对象的特性被大肆宣扬,大量书籍介绍其面向对象的特性;着实,面向对象技术具有较好的应对复杂性问题的功能,相比面向过程语言,在大规模的软件工程,面向对象语言对软件复用和扩展性都具有较好的支持。另一方面,C++对泛型编程也具有优秀的支持能力,其允许对操作符重定义的特性,使得我们在一个较高的抽象层次对程序[数据+操作
]进行思考和建模,而最终的使用接口在不失去运行效率的情况下,又具有出奇的一致性和透明性,这对软件复用来说是多么重要!
泛型思维允许将操作和类型解耦开,使得同一个操作可以对不同类型进行;而面向对象是将操作和类型聚合到一起。这是两个截然不同的抽象方向。他们各有优缺点和使用场景。在业务框架或业务场景领域更倾向于使用面向对象思想,比如MFC, Spring, QT;而在科学计算和数学等基础软件库更适合用泛型思维来建模,比如STL, GLM, Eigen等。
虚函数机制
和泛型思维的函数重载,类模板偏特化和完全特化,函数完全特化
都具有多态能力,一种是动多态,一种是静多态。科学计算和数学等基础软件库对运行效率有极致的要求,同时其数据结构相对固定和单一,比如GLM / Eigen库主要的数据只有向量和矩阵两种类型,而如果采用面向对象的思维建模势必会用到虚函数机制,这会导致运行时的效率问题,而泛型思维方式在编译时实现静多态的功能,不会有运行时的代价。二者的区别决定了他们分别的应用领域。
C++语言对泛型编程的支持,强调将操作和数据分开,操作是处理步骤有序集合,数据是用来对现实世界静态状态建模的结果。操作可以改变数据的状态,是对现实中数据之间交互的建模。什么时候触发操作,或操作以怎样的规则操作数据,这叫策略或policy。个人观点,操作+策略组成具体算法
。
操作在STL中叫做算法,数据是存储在容器中的,策略是用函数对象体现。
举个例子,某个公司有一群员工,从业务角度出发,比如我要作人员结构调整和优化,我需要知道超过40岁的都有谁?我希望能够按照年龄大小对员工进行排序,然后把年龄大于40的员工找出来进行特别关照。
我可以将员工信息建模为类型Employee,每个员工都是Employee的实例,将这些实例存储在STL的容器中,然后用STL的算法按照年龄大小的策略
进行排序。排序完成后,后续的查找大于40的员工
的操作就很容易进行。翻译为代码就是:
class Employee {
public:
// 省略构造函数
int Age() const { return age_; }
bool IsMale() const { return is_male_; }
// 其他方法
// ...
private:
int age_;
bool is_male_;
// 其他属性
// ...
}
// 客户程序
void Process() {
// 将员工信息存储在STL的list或vector中
std::list<Employee> employee_list{xxxx};
// or std::vector employee_list{xxxx};
// 策略 -- 按年龄升序排序,也可以按照性别啊,绩效高低,会不会打篮球等策略。
auto policy = [](const Employee& a, const Employee& b) -> bool {
return a.Age() < b.Age();
};
std::sort(employee_list.begin(), employee_list.end(), policy);
// 后续查找等操作
// ...
}
再比如,我要从这些员工里面找到第一个满足某个条件的人,比如叫Lera的人
;年龄为25岁且是famale的人
;绩效为X++的人
可以这样
auto policy = [](const Employee& employee) {
return employee.name() == "Lera";
// or return employee.Age() == 25 && !employee.IsMale();
// or return employee.Performance() == Perf("X++");
};
auto location = std::find_if(employee_list.begin(), employee_list.end(), policy);
std::vector
和std::list
从C++语法和实现细节来讲是两种不同的类型,但是从更抽象的层次考虑,他们却又无多大区别,都是容器,容器是什么? – 置物之所,都是用来存储东西的。如果我们想从容器里面找一个(find
)满足某种条件的对象,比如相等 operator==(a, b) 为true
的一个对象,这个操作应该与[东西是存储在std::vector
还是std::list
容器中] 独立。为了获得这种独立性,必须进行抽象,在这里就是对容器的遍历进行了抽象,将遍历具体容器的过程抽象出来,引入一个叫迭代器
的东西,我们将寻找某个满足条件的元素
的这种操作统称为算法
,把放东西的玩意叫做容器
,通过引入一个中间层迭代器
将算法
和容器
解耦开来,使得两者可以独立发展。而迭代器
作为中间人给算法
和容器
搭建了一座互通的桥梁。如上面代码所示,
STL定义了迭代器的五种型别特性,如下:
template<typename _Category, typename _Tp, typename _Distance = ptrdiff_t,
typename _Pointer = _Tp*, typename _Reference = _Tp&>
struct iterator
{
/// One of the @link iterator_tags tag types@endlink.
typedef _Category iterator_category;
/// The type "pointed to" by the iterator.
typedef _Tp value_type;
/// Distance between iterators is represented as this type.
typedef _Distance difference_type;
/// This type represents a pointer-to-value_type.
typedef _Pointer pointer;
/// This type represents a reference-to-value_type.
typedef _Reference reference;
};
迭代器有分类_Category,所指对象的类型,引用,指针,迭代器距离类型等。我们终点关注下_Category, _Category是为了标示迭代器与访问方式
有关的特性。 STL又定义了输入迭代器,输出迭代器,前向迭代器,双向迭代器和随机迭代器类别。
/**
* @defgroup iterator_tags Iterator Tags
* These are empty types, used to distinguish different iterators. The
* distinction is not made by what they contain, but simply by what they
* are. Different underlying algorithms can then be used based on the
* different operations supported by different iterator types.
*/
//@{
/// Marking input iterators.
struct input_iterator_tag { };
/// Marking output iterators.
struct output_iterator_tag { };
/// Forward iterators support a superset of input iterator operations.
struct forward_iterator_tag : public input_iterator_tag { };
/// Bidirectional iterators support a superset of forward iterator
/// operations.
struct bidirectional_iterator_tag : public forward_iterator_tag { };
/// Random-access iterators support a superset of bidirectional
/// iterator operations.
struct random_access_iterator_tag : public bidirectional_iterator_tag { };
//@}
假设我们有一个算法来计算迭代器距离,函数原型如下:
template <typename Iterator>
size_t distance(Iterator first, Iterator last) {
....
实现细节 暂时为空,稍后补全。
....
}
我们知道C++中数组是连续内存存储,如果计算两个指向原生数组的迭代器(原生指针是一种具有random_access_iterator_tag
特性的迭代器)。计算其距离只需要迭代器相减就可以获得,这时候,distance
算法的复杂度为O(1)。但如果迭代器不具有random_access_iterator_tag
特性。那么distance
只能通过所有迭代器都支持的++
操作一步步从first前进到last。这时候distance
算法的复杂度为O(n)。因此我们可以利用迭代器的与[访问方式
]有关的型别特性来对迭代器行走操作相关的算法进行深度优化。
random_access_iterator_tag
型别特性的迭代器,distance
算法的实现]更普遍的实现方式,支持所有类型的迭代器。
template <typename Iterator>
size_t __distance(Iterator first, Iterator last, std::input_iterator_tag) {
size_t count = 0;
while(first != last) {
++count; // *************** 版本 1
++first;
}
return count;
}
这种实现方式可以支持任意类型的迭代器,是最泛化的distance
版本。
random_access_iterator_tag
型别特性的迭代器,distance
算法的实现]如果我们有两个随机具有 Employee* p1, Employee* p2
指向数组std::vector
的第一个和最后一个元素的下一个位置
arr[0] | arr[1] | arr[2] | arr[3] | arr[4] | arr[5] | !arr[6]! |
---|
std::vector<Employee> arr(6)
Employee* first = &arr[0];
Employee* last = &arr[6];
记住原生指针也是一种迭代器 ------ 强化了的随机访问迭代器。按照STL的语言,原生指针这种迭代器具有std::random_access_iterator_tag的型别特性。如果是这种迭代器,那么算法 distance(...)
该怎么实现最高效呢,当然是如下:
template <typename RandomAccessIterator>
size_t __distance(RandomAccessIterator first, RandomAccessIterator last, std::random_access_iterator_tag) {
return last - first; // *************** 版本 2
}
具有random_access_iterator_tag
型别特性的迭代器,distance
算法的实现用版本 1
的算法也没有毛病,只是效率低一些。但是对于一些不支持随机访问的迭代器类型,也只能这么实现。比如std::list
or std::forward_list
算法版本 2就不能工作了。只能老老实实采用版本 1的算法,一步一步从头走到尾。
是否存在一种方式,能够在编译时候告诉distance(...)
算法:你即将操作的迭代器对象具有随机访问的型别特性,你可以采用优化版的算法1
;
当然可以!!! 我们结合型别萃取(iterator_traits) + 函数重载派送机制来让编译器根据型别特性替我们选择是调用优化版的算法还是更通用的算法。
在STL中,定义了一个用于辨识迭代器型别特性的类,可以萃取出迭代器的型别特性,对于distance
算法,我们关心的是迭代器的型别特性iterator_category
,如下是该类针对原生指针的特化版本。如我们所想,对于原生指针,其型别特性是random_access_iterator_tag
。
/// Partial specialization for pointer types.
template<typename _Tp>
struct iterator_traits<_Tp*>
{
typedef random_access_iterator_tag iterator_category;
typedef _Tp value_type;
typedef ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef _Tp& reference;
};
我们可以通过如下方式获得迭代器型别特性的类型并创建实例:
// 对于std::vector容器
std::iterator_traits<VectorIterator>::iterator_category instacne1;
// 对于std::vector容器
std::iterator_traits<ListIterator>::iterator_category instacne2;
显然instacne1
和 instacne2
是属于不同类的对象。由于他们隶属不同的class,我们可以用instacne1
和 instacne2
来触发编译器多重载函数的派送机制,调用不同版本的__distance()
算法。举个例子:
// 定义两个重载函数
class A{};
class B{};
void func(int first, int last ,A a) {} // version 1
void func(int first, int last ,B a) {} // version 2
void clent() {
A a;
B b;
func(0, 1, a); // 编译器有能力获得a对象所属类型并调用 version 1 的函数
func(0, 1, b); // call version 2
}
将上面介绍的内容整合到一起,形成最终的用户接口;
template <typename Iterator>
size_t distance(Iterator first, Iterator last) {
typedef typename iterator_traits<Iterator>::iterator_category iterator_category;
// 这里第三个参数 iterator_category()匿名对象可以触发重载函数的派送机制
// 根据第三个参数的类型是std::random_access_iterator_tag 或者不是,派送到两个__distance(...)辅助函数之一。
return __distance(first, last, iterator_category()); // iterator_category() 是匿名对象,只是用来触发重载函数的派遣机制。
}
今天分享的内容,我将用到的完整代码整理如下,内容来自STL,就构成了一个完整的计算迭代器距离distance(first, last)
的聪明的算法。
// 泛化版本
template <typename Iterator>
size_t __distance(Iterator first, Iterator last, std::input_iterator_tag) {
size_t count = 0;
while(first != last) {
++count; // *************** 版本 1
++first;
}
return count;
}
// 针对随机访问的迭代器特化版本
template <typename Iterator>
template <typename Iterator>
size_t __distance(Iterator first, Iterator last, std::random_access_iterator_tag) {
return last - first; // *************** 版本 2
}
// user interface
template <typename Iterator>
size_t distance(Iterator first, Iterator last) {
typedef typename std::iterator_traits<Iterator>::iterator_category iterator_category
return __distance(first, last, iterator_category());
}
下次分享预计会基于迭代器型别特性iterator_traits
扩展到更泛化的类型型别特性type_traits
,分析其技术手段和能解决的问题,并会给出一个有关数值计算的优化版算法。