迭代器(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是迭代器的分类。
根据移动特性和施行的操作,迭代器被分为五类:
- Input iterator:这种迭代器所指的对象,不允许外界改变,只读
- Output iterator:只写
- Forward Iterator:允许“写入型”算法在此种迭代器所形成的区间上进行读写操作。
- Bidiretional Iterator:可双向移动,某些算法需要逆向走访某个迭代器区间(例如逆向拷贝某范围的元素),可以使用Bidirectional Iterator。
- Romdom Access Iterator:前四种迭代器都只供应一部分指针的算术能力,第五种则覆盖所有指针的算术能力,p+n,p-n,p[n],p1-p2,p1<p2
其继承关系如图:
在实际的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类型在定位具体模板的同时,它们的继承关系也可以被利用上。
比如,假如有这样的继承关系:
- B类型,用来定位InputIterator版本的算法
- D1类型(继承了B),用来定位ForwardIterator版本的算法
- 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为:
- forward_iterator_tag
- 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关注的是:
- has_trivial_default_constructor
- has_trivial_copy_constructor
- has_trivial_assignment_operator
- has_trivial_destructor
- is_POD_type
它们的值在以下两个类型中取,避免语义的混淆
struct __true_type{};
struct __false_type{};
通过判断构造、析构、拷贝、赋值等操作的支持情况,为类型选择最高效的处理方式。这对于大规模而操作频繁的容器,有显著的效率提升。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗