《STL源码剖析》---迭代器章节读书笔记

Posted on 2024-07-23 23:23  Aderversa  阅读(4)  评论(0编辑  收藏  举报

迭代器(Iterator)

STL的核心思想是:将数据容器和算法分开,彼此独立设计,最后再用一贴胶着剂将它们撮合在一起。

find()​算法为例:

template<class InputIterator, class T>
InputIterator find(
	InputIterator first,
	InputIterator last,
	const T& value
);
{
	while(first != last && *first != value)
		++first;
	return first;
}

迭代器没有我们想象中的复杂,这里我们可以把迭代器理解为:只需要进行++​操作,迭代器就会指向下一个元素。而*​运算符用来取出迭代器指向的元素。

重载操作符是C++为了让对象尽可能地模仿原生语言的语法而搞出来的东西,不使用其实也是没有问题的(比如Java,没有重载运算符,就可以用其他函数替代,只不过不再默认可以通过运算符进行某些操作。)

我觉得C++重载运算符这些东西可能会无形之中增加开发者的工作量。

只需要给予不同的迭代器,find()​就能够查找不同的数据容器。

这里要注意的是,find()并不会判断迭代器是否支持某些方法,也就是迭代器是否支持某些接口。

find()通过模板,默认迭代器实现了某些方法(如:++操作符,*操作符)。

如果不借助模板,那就有可能需要定义一个关于迭代器接口的抽象类,然后find()接收任何实现了该接口的对象。

但这样find()就会产生对迭代器接口的依赖。

但是C++借助模板和C++定义好的标准(接口没有写在实质的代码里,而是在规定里),实现了算法和迭代器的依赖分离。至少我是这样认为的

于是我们可以通过这样的方法来遍历容器:

list<int>::iterator it = find(myList.begin(), myList.end(), 10);
if (it != myList.end())
	std::cout << *it << std::endl;
else
	std::cout << "Not find" << std::endl;

类似于指针?

如果你详细学习过C语言里面的指针,你肯定知道用指针来遍历一个顺序数组是完全可行的!

int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* first = &arr[0];
int* last = &arr[0] + 10;
while (first != last && *first != 6)
	++first;

这C++中的迭代器跟指针就好像是一个模子刻出来的。

但是,指针遍历顺序的数组。而迭代器则是不管数据被怎么组织,它都能够把每一个元素都遍历得到。

假设我们设计了一个自己的链表List类,我们想要find()​算法能够应用在List中,该如何做呢?

template<class T>
class List {
public:
	void insert_front(T value);
	void insert_end(T value);
	void display(std::ostream& os = std::cout) const;
	...
private:
	ListItem<T>* _end;
	ListItem<T>* _front;
	long _size;
};

template<class T
class ListItem {
public:
	T value() const {return _value;}
	ListItem* next() const {return _next;}
	...
private:
	T _value;
	ListItem* _next;
};

容器我们已经有了,但是如何为它配备一个迭代器呢?

template<class Item> // Item是容器存储的元素类型
struct ListIter {
	Item* ptr;
	ListItem(Item* p = nullptr) : ptr(p) {}
	Item& operator*() const {return *ptr;}
	Item* operator->() const {return ptr;}

	ListIter& operator++(){ // 前置++
		ptr = ptr->next(); 
		return *this;
	}

	ListIter& operator++(int){ // 后置++,int参数不使用,仅仅是为了能表示后置++
		ListIter tmp = *this;
		++*this;
		return tmp;
	}

	bool operator==(const ListIter& i) const {
		return ptr == i.ptr;
	}

	bool operator!=(const ListIter& i) const {
		return ptr != i.ptr;
	}
};


OK,上面这个就是我们的List的迭代器的全貌啦。我们可以这样使用上面这个迭代器:

// 由于find()中使用了,*iter != value
// 而*iter的类型是ListItem<int>,而value的类型是int
// 所以,需要支持这两者的!=操作符,否则find()无法工作。
template<class T> 
bool operator!=(const ListItem<T>& item, T n) {
	return item.value() != n;
}

List<int> myList;
for (int i = 0; i < 5; i++)
	myList.insert_end(i);
ListIter<ListItem<int>> begin(myList.front());
ListIter<ListItem<int>> end;  // 默认为0
ListIter<ListItem<int>> iter; // 默认为0
iter = find(begin, end, 3);

对比与标准库的使用是:list<int>::iterator first = myList.begin();

我们的迭代器需要指定本应对用户透明的:ListItem<>​。如果封装好了List类,那用户就不应该能访问到这个ListItem<>​。

但是这样的话,我们的为List配置的迭代器ListIter就无法使用了。

因此,ListIter不应该由用户来编写,而是由List类的开发者来编写。只有最清楚List内部实现的人,才知道List的迭代器如何实现!在更为复杂的数据容器中就更是如此!

这样容器的所有细节都可以不暴露给外部。这也是STL中每一种容器都有专属的迭代器的原因。

相应类型?

假设有这样一个算法,它的实现中需要使用到迭代器所指内容的类型,但是该算法的参数列表中没有指定。

template<class I>
void func(I first, I end) {
	typeof(*first) var;
	// 各种操作
}

如果算法中必须要使用迭代器所指内容的类型来声明局部变量,哪问题是这个类型从哪来?C++中没有typeof()​这种东西啊。。。。

一种方法是借助模板来获取变量的类型:

template<class I, class T>
void func_impl(I first, I end, T n) {
	T var;
	// 各种操作
}
template<class I>
void func(I first, I end) {
	func_impl(first, end, *first);
}

通过这种方法,我们可以利用迭代器所指对象的类型。

但是,由于算法中常用的相应类型不只有“迭代器所指对象的类型”,还有其他4中类型,而这些类型不总是可以通过上述方法来解决。

所以我们需要探索一种更加全面的解决方案。

Traits编程法 --- STL源码门钥

我们称迭代器所指对象的类型为value_type​。

如果只是将value_type作为函数的参数,那还可以使用上述的方法来解决问题。

但如果要将value_type作为一个返回值,那就麻烦大了。

那我不可以用auto自动推导吗?感觉可以但没试过,C++在2011年才加入auto自动类型推导,而《STL源码剖析》是2002年出版的。

我们就先按照前辈开发者的思路来。

我们可将迭代器所指对象的类型,嵌入到迭代器的类中:

template <class T>
struct MyIter {
    typedef T value_type;
	T* ptr;
	// ...
};

那我们的算法就可以这样子使用这个迭代器所指对象的类型:

template<class I>
typename I::value_type // 这一行是fun()的返回值类型。由于编译器不知道I::value_type是一个类型,如果不显示声明,它分辨不了这是个变量还是类型
fun(I first, I end) {
	I::value_type var;
	// 算法操作
	return var;
}

但是这样做有一个陷阱,那就是:如果迭代器为指针,那么指针没有value_type这个东西!

模板偏特化

所谓模板偏特化,我们可以理解为:针对(任何)template参数更进一步的条件限制所设计出来的一个特化版本。

我们可以为模板函数fun()​特化出来一个接收指针的fun()​,这样指针就可以应用fun()​.

template<class I>
I fun(I* first, I* end);

但是这样有一个问题:当需要特化的东西多了,那fun()​就会产生很多份重复。

即使fun()​代码量不大,但是有太多分重复还是会导致修改fun()​变得困难起来。

既然特化只是为了获取“相应类型”,那有没有可能我们可以将获取相应类型的过程变成一个统一的方法。这样func不需要管传进来的是迭代器还是指针,它都可以正确地获取相应类型。

实际情况就是,STL使用了class template来编写一个iterator_traits​来对相应类型进行“萃取”。

template<class I>
struct iterator_traits {
	typedef typename I::value_type value_type;
};
// 对指针类型进行偏特化
template<class I>
struct iterator_traits<I*> {
	typedef T value_type;
};
// 通过这样方法来“萃取”类型
iterator_traits<int*>::value_type var;

但是还是不够,有些类型还是不能正确的萃取出来:

iterator_traits<const int*>::value_type var; // 萃取出来的是const int,但我们需要int作为局部变量(会对其进行修改)

这需要我们进一步声明一个条件限制更强的偏特化iterator_traits:

template<class I>
struct iterator_traits<const I*> {
	typedef I value_type;
};

这样的话,我们的iterator_traits就成为了一台类型萃取机,提供了一层间接性,将相应类型的获取方式变得统一:

常用的迭代器相应类型有五种,每一个迭代器类型都应该提供这些相应类型,以保证和STL算法的兼容:

template<class I>
struct iterator_traits {
	typedef typename I::value_type value_type;
	typedef typename I::difference_type difference_type;
	typedef typename I::pointer pointer;
	typedef typename I::reference reference;
	typedef typename I::iterator_category iterator_category;
};

这个iterator_traits​需要对pointer和const-pointer进行偏特化。

value_type

value_type是迭代器所指对象的类型。

difference_type

difference_type用来表示两个迭代器之间的距离,因此它也可以用来表示一个容器的最大容量。

如果一个泛型算法提供计数功能,例如STL的count(),其返回值就必须使用difference_type.

template <class I, class T>
typename iterator_traits<I>::difference_type
count(I first, I last, const T& value){
	typename iterator_traits<I>::difference_type n = 0;
	for(;first != last; ++first) {
		if (*first == value) {
			++n;
		}
	}
	return n;
}

需要为pointer和const-pointer进行iterator_traits的偏特化。

template <class T>
struct iterator_traits<T*> {
	...
	typedef ptrdiff_t difference_type;
};
template <class T>
struct iterator_traits<const T*> {
	...
	typedef ptrdiff_t difference_type;
};

reference type

如果要返回左值,就应该以reference的方式返回。所以需要知道引用的类型。

当p是一个允许改变所指对象的内容的迭代器时,*p的类型不应该是T,而是T&。

很好理解,如果*p解引用出来不是引用,那原对象根本得不到修改。

pointer type

我们也可以传回一个pointer,指向迭代器所指之物。

Item& operator*() const { return *ptr; }
Item* operator->() const { return ptr; }

前者就是前面的reference type的返回左值。

后者就是返回pointer,如果->操作符不是返回pointer,

对于reference type和pointer type,iterator_traits需要提供对于pointer和const-pointer的偏特化:

template<class T>
struct iterator_traits<T*> {
	typedef T* pointer;
	typedef T& reference;
};
template<class T>
struct iterator_traits<const T*> {
	typedef const T* pointer;
	typedef const T& reference;
};

iterator_category

iterator_category是迭代器的分类。

根据移动特性和施行的操作,迭代器被分为五类:

  1. Input iterator:这种迭代器所指的对象,不允许外界改变,只读
  2. Output iterator:只写
  3. Forward Iterator:允许“写入型”算法在此种迭代器所形成的区间上进行读写操作。
  4. Bidiretional Iterator:可双向移动,某些算法需要逆向走访某个迭代器区间(例如逆向拷贝某范围的元素),可以使用Bidirectional Iterator。
  5. Romdom Access Iterator:前四种迭代器都只供应一部分指针的算术能力,第五种则覆盖所有指针的算术能力,p+n,p-n,p[n],p1-p2,p1<p2

其继承关系如图:

flowchart TD a["Input Iterator"] --> b["Forward Iterator"] c["Output Iterator"] --> b b --> d["Bidirectional Iterator"] d --> e["Random Access Iterator"]

在实际的STL算法实现中,算法需要考虑到不同迭代器对一个算法的效率产生的影响,虽然有时候接收Forward Iterator的算法,用户可以传递Random Access Iterator。但如果用户传入Random Access Iterator而算法没有任何的效率上的改进,那这样的话是没有意义的。

我们必须利用到不同迭代器的特性,在传入不同的迭代器时有不同的效率表现。尽可能利用到不同迭代器类型带来的好处。

比如我们有这样一个算法:

template<class InputIterator, class Distance>
void advance_II(InputIterator& i, Distance n)
{
	while(n--) ++i;
}

对于InputIterator,它必须以O(N)的复杂度完成工作。

而如果我们传入Random Access Iterator,它就可以通过算术运算完成前进的操作:

template<class RandomAccessIterator, class Distance>
void advance_RAI(RandomAccessIterator& i, Distance n)
{
	i += n;
}

这样的话,advance就可以以O(1)的复杂度完成工作。

充分利用到了不同迭代器的特性。

那我们的算法该如何确定使用哪一个版本的advance()呢?

我们可以定义一些实际上没有意义,但是可以用于特化模板的类型(我称之为Tag):

它们存在的意义就是为了让程序能够定位某个类型,要使用哪个模板函数:

struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag : public input_iterator_tag{};
struct bidirectional_iterator_tag : public forward_iterator_tag{};
struct random_access_iterator_tag : public bidirectional_iteration_tag{};

template<class InputIterator, class Distance>
inline void __advance(InputIterator& i, Distance n, input_iterator_tag)
{
	while(n--) ++i;
}

template<class ForwardIterator, class Distance>
inline void __advance(ForwardIterator& i, Distance n, forward_iterator_tag)
{
	while(n--) ++i;
}

template<class BidirectionalIterator, class Distance>
inline void __advance(BidirectionalIterator& i, Distance n, bidirectional_iterator_tag)
{
	while(n--) ++i;
}

template<class RandomAccessIterator, class Distance>
inline void __advance(RandomAccessIteratorr& i, Distance n, random_access_iterator_tag)
{
	while(n--) ++i;
}

这样我们就可以将advance写成这样,在编译时期就确定执行哪个函数:

template<class InputIterator, class Distance>
inline void advance(InputIterator& i, Distance n)
{
	__advance(i, n, iterator_traits<InputIterator>::iterator_category());
	// 多加一个()是因为要初始化一个对象,传递到__advance(),这个临时的对象除了用来定位__advance()没有其他用处。
}

iterator_category就是我们上面定义的五种用于定位具体__advance()。

这样子的话,迭代器Iterator必须提供迭代器的类型:

template<class I>
struct iterator_traits {
	...
	typedef typename I::iterator_category iterator_category;
};

// pointer的偏特化
template<class I>
struct iterator_traits<I*> {
	...
	typedef random_access_iterator_tag iterator_category;// pointer是一种随机访问指针
};

// const-pointer的偏特化
template<class I>
struct iterator_traits<const I*> {
	...
	typedef random_access_iterator_tag iterator_category;// pointer是一种随机访问指针
};

任何一个迭代器类型,应该落在最强化的那一个。而在算法要求的迭代器中,应该给出接收最弱的哪个迭代器是什么迭代器类型的。

消除“单纯传递调用的函数”

前面我们用类似于迭代器之间的继承关系来定义我们的Tag类型的。

这些Tag类型在定位具体模板的同时,它们的继承关系也可以被利用上。

比如,假如有这样的继承关系:

  1. B类型,用来定位InputIterator版本的算法
  2. D1类型(继承了B),用来定位ForwardIterator版本的算法
  3. D2类型(继承了D1),用来定位BidirectionalIterator版本的算法

假如有这样的算法:

template<class I>
void func(I& p, B)
{
	cout << "B version\n";
}

template<class I>
void func(I& p, D2)
{
	cout << "D2 version\n";
}

假设D1类型的算法和B类型的算法都是一样的,也就是说在B算法的基础上,即使你变成了D1,有了D1的一些功能,算法也依旧不能做出改进,那就不需要给D1专门重载一个func出来。

但当到了D2,算法可以有大改动,因此就可以专门定义func来利用D2类型来精准定位。而D1则和B进行模糊定位,B本身则是被精准定位。

虽然不太理解《STL源码剖析》的“消除单纯传递调用”是什么意思,但从他所给的例子我可以得出上述结论。

以distance()为例

我们知道不同的迭代器计算距离可能会有所不同,比如ForwardIterator就需要逐步++来计算距离:

template<class InputIterator>
inline iterator_traits<InputIterator>::difference_type
__distance(InputIterator first, InputIterator last,
		   input_iterator_tag) {
	iterator_traits<InputIterator>::difference_type n = 0;
	while(first != last){
		++first;
		++n;
	}
	return n;
}

由于存在继承关系,tag为:

  1. forward_iterator_tag
  2. bidirectional_iterator_tag

都将定位到上面这个__distance()

但是,RandomAccessIterator就直接了许多:

template<class RandomAccessIterator>
inline iterator_traits<RandomAccessIterator>::difference_type
__distance(RandomAccessIterator first, RandomAccessIterator last,
		   random_access_iterator_tag) {
	return last - first;
}

以上都是利用Tag类型,让程序能在编译器就确定走哪个重载模板函数的方法。

std::iterator的保证

我们可以这样认为:STL的算法统统都会使用traits进行萃取,所以我们的迭代器必须准备好以下这几个typedef,以适配STL算法的萃取。

template<class Category
	     class T
 		 class Distance = ptrdiff_t
		 class Pointer = T*
		 class Reference = T&>
struct iterator{
	typedef Category  iterator_category;
	typedef T         value_type;
	typedef Distance  difference_type;
	typedef Pointer   pointer;
	typedef Reference reference;
};

上述东西是STL提供的一个iterator class,我们所写的东西通过继承这个iterator class就可以定义一些STL标准所需要的东西。

只需要指定我们的iterator的迭代器类型(是ForwardIterator还是InputIterator等等)和迭代器所指的元素类型,即可使用。

SGI STL的私房菜:__type_traits

traits编程技法很棒,适度弥补了C++语言本身的不足(C++不是强类型语言)。

而SGI STL把这种tratis技法发挥得很好。

如果说iterator_traits是用来萃取迭代器的特性的话,__type_traits就是用来萃取类型的特性。

iterator_traits所萃取的特性是类中的typedef,如:value_type, reference等。

那__type_traits萃取的也是某些typedef?既然不是标准,那命名规范怎么来?它只是应用在自己的STL实现中而不被普通用户所见吗?

普通用户应该是不会关心这些__type_traits这个东西,所以这玩意是为了STL而服务的。

而了解SGI STL实现的人员可以利用上这方面的特性从而提供自己程序的性能。

__type_traits关注的是:

  1. has_trivial_default_constructor
  2. has_trivial_copy_constructor
  3. has_trivial_assignment_operator
  4. has_trivial_destructor
  5. is_POD_type

它们的值在以下两个类型中取,避免语义的混淆

struct __true_type{};
struct __false_type{};

通过判断构造、析构、拷贝、赋值等操作的支持情况,为类型选择最高效的处理方式。这对于大规模而操作频繁的容器,有显著的效率提升。

Copyright © 2024 Aderversa
Powered by .NET 8.0 on Kubernetes