百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

C++ |STL算法如何萃取(traits)迭代器型别(value_type)?

toqiye 2024-12-16 16:18 20 浏览 0 评论

STL为什么需要迭代器?看下面实例:

//对于int类的求和函数
int sum(int *a , int n)
{
    int sum = 0 ;
    for (int i = 0 ; i < n ; i++) {
        sum += *a++;
    }
    return sum;
}
//对于listNode类的求和函数
struct ListNode {
    int val;
    ListNode * next;
};
int sum(ListNode * head) {
    int sum = 0;
    ListNode *p = head;
    while (p != NULL) {
        sum += p->val;
        p=p->next;
    }
    return sum;
}

上面两个函数存在如下设计缺陷:

I 遍历int数组和单链表listNode时,需要设计两份不一样的sum求和函数,对于STL这样含有大量容器的代码库,针对每一种容器都设计sum的话,过于冗杂。

II 在sum函数中暴露了太多设计细节,如ListNode的节点值类型int,和指向下一个节点的指针next。

III 对于int数组来说,还必须知道数组的大小,以免越界访问。

IV 算法的设计过多的依赖容器,容器的改动会造成大量算法函数的随之改动。

那么,如何设计才能使得算法摆脱特定容器?如何让算法和容器各自独立设计,互不影响又能统一到一起?

STL 迭代器是一种抽象的设计概念,在设计模式中是这么定义迭代器模式的,即提供一种方法,使之能够巡访某个聚合物所含的每一个元素,而无需暴露该聚合物的内部表述方式。

不论是泛型思维或STL的实际运用,迭代器都扮演着重要的角色,STL的中心思想就在于:将数据容器和算法分开,彼此独立设计,最后再以一帖胶着剂将它们撮合在一起。

谈到迭代器需要遍历容器就想到指针,的确,迭代器就是一种类似指针的对象,而指针的各种行为中最常见也最重要的就是内容提领(dereference)和成员访问(member access),因此,迭代器最重要的编程工作就是对operator*和operator->进行重载工作。

我们先回过头去看看sum函数,在sum函数中,我们必须知道容器的元素类型(型别),这关系到函数返回值。

1 函数模板参数推导机制

看下面的代码:

#include <stdio.h>
#include <iostream>
using namespace std;

// 此函数中并不知道iter所指的元素型别,而是通过模板参数 T 来获取的
template <class I, class T1 ,class T> // 这里要用 T 来表示 I 的型别,T用*I 赋值
T sum_impl(I iter ,T1 n , T t) {
    T sum = 0; // 通过模板的特性,可以获取I所指之物的型别,此处为int

    // 这里做func应该做的工作
    for(int i = 0 ; i < n ;i++){
        sum+=*iter++;
    } 
    return sum;
}

template <class I , class T1>
inline T1 sum(I iter , T1 n) {// 此处暴露了template参数推导机制的缺陷,iv 型别用于返回值时便束手无策
    return sum_impl(iter , n ,*iter);
}

int main() {
    int a[5] = {1,2,3,4,5};
    int sum1 = sum(a , 5);
    cout<<sum1<<endl;
}

通过函数模板的参数推导机制推导出了iter所指之物的型别(int),于是可以定义T sum变量。

对于返回值类型,template参数推导机制也束手无策。

2 类模板内嵌型别机制

在类中可以进行型别声明。

看下面的代码:

#include <stdio.h>
#include <iostream>
using namespace std;

// 定义一个简单的iterator类模板
template <class T>
struct MyIter{
    typedef T value_type; // 内嵌型别声明
    T* ptr;
    MyIter(T* p =0):ptr(p){}
    T& operator*() const {return *ptr;}
};

// 这样当我们使用myIter类型时,可以通过 myIter::value_type来
// 获得相应的myIter所指向的类型(型别)
template <class I>
typename I::value_type // func返回值型别,也是定义的value_type的类型I的型别
func(I iter){
    return *iter;
}

int main(){
    MyIter<int> iter(new int(8));
    cout<<func(iter)<<endl;  // 8
}

注意,func()函数的返回值型别必须加上关键词typename,因为T是一个template参数,在它被编译器具现化之前,编译器对其一无所知,关键词typename的用意在于告诉编译器这是一个型别,如此才能通过编译。

内嵌型别看起来不错,可以很顺利的提取出迭代器所指型别并克服了template参数推导机制不能用于函数返回值的缺陷。

如果不是class type,那么就无法声明内嵌型别。但是STL绝对必须接受原生指针(如vector)作为一个迭代器。因此,必须另行它法!

3 类模板偏特化机制

针对原生指针这类特殊情况,我们很容易想到利用模板偏特化的机制来实现特殊声明,在泛化设计中提供一个特化版本。偏特化的定义是:针对任何template参数更进一步的条件限制所设计出来的一个特化版本。这里,针对上面的MyIter设计出一个适合原生指针的特化版本,如下:

template <class T>
struct MyIter <T*>{   // T*代表T为原生指针,这便是T为任意型别的一个更进一步的条件限制
    typedef T value_type; // 内嵌型别声明
    T* ptr;
    MyIter(T* p =0):ptr(p){}
    T& operator*() const {return *ptr;}
};

这样,函数func()就可以写为:

template <class I>
I func(I* iter){
    return *iter;
}

以上针对类模板内嵌型别机制与偏特化机制要分别实现模板函数func(),太繁杂,太不优雅了。

4 iterator_traits中间层机制

我们可以不直接使用myIter的value_type,而是通过另一个类来把这个信息提取出来:

// 用于traits出迭代器所指对象的型别
template <class I> // 这里的 I 可以是任意类型的迭代器
struct iterator_traits
{
  typedef typename I::value_type value_type;
};

这样,我们可以通过 iterator_traits<myIter>::value_type 来获得myIter的value_type,于是我们把func函数改写成:

template <class I> // 这里的I可以是任意类型的迭代器
typename iterator_traits<I>::value_type // 通过iterator_traits类萃取I的型别
func(I iter){
    return *iter;
}

从表面上来看,这么做只是多了一个间接层,但是带来的好处是极大的!iterator_traits类可以拥有特化版本,如下:

// 原生指针特化版本
template <class T>
struct iterator_traits <T*>
{
  typedef T  value_type;
}
// const指针特化版本
template <class T>
struct iterator_traits <const T*>
{
  typedef T  value_type;
}

于是,原生指针int*虽然不是一种class type,也可以通过traits取其value,到这里traits的思想就基本明了了,以下是测试代码:

#include <iostream>

template <class T>   // iterator_traits萃取机
struct iterator_traits {
    typedef typename T::value_type value_type;
};

template <class T>    // iterator_traits萃取机特化T*
struct iterator_traits<T*> {
    typedef T value_type;
};

template <class T>    // iterator_traits萃取机特化const T*
struct iterator_traits<const T*> { 
    typedef T value_type;
};

template <class T>  // iterator迭代器
struct MyIter {
    typedef T value_type;
    T * ptr;
    MyIter(T * p = 0) : ptr (p) {};
    T& operator* () const { return *ptr;}
};

template <class I>  // 算法(函数模板)定义
typename iterator_traits<I>::value_type
func (I ite) {
    return *ite;
}

int main() {
    int i = 3;
    const int k = 3;
    MyIter<int> v(&i);
    std::cout << func(v) << std::endl;    // 3
    std::cout << func(&i) << std::endl;  // 3
    std::cout << func(&k) << std::endl;  // 3
    return 0;
}

通过定义内嵌类型,我们获得了知晓iterator所指元素类型的方法,通过封装中间层(iterator_traits类模板),我们将函数模板对于原生指针和自定义iterator的定义都统一起来了。

这就是traits技法的妙处所在。

下面就来看看STL中”萃取机“的源码:

/**
 * 用于标记迭代器类型
 */
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_iterator_tag {};

/**
 * 用于traits出迭代其所指对象的型别
 */
template <class Iterator>
struct iterator_traits
{
  // 迭代器类型, STL提供五种迭代器
  typedef typename Iterator::iterator_category iterator_category;

  // 迭代器所指对象的型别
  // 如果想与STL算法兼容, 那么在类内需要提供value_type定义
  typedef typename Iterator::value_type        value_type;

  // 这个是用于处理两个迭代器间距离的类型
  typedef typename Iterator::difference_type   difference_type;

  // 直接指向对象的原生指针类型
  typedef typename Iterator::pointer           pointer;

  // 这个是对象的引用类型
  typedef typename Iterator::reference         reference;
};

/**
 * 针对指针提供特化版本
 */
template <class T>
struct iterator_traits<T*>
{
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef ptrdiff_t                  difference_type;
  typedef T*                         pointer;
  typedef T&                         reference;
};
/**
 * 针对指向常对象的指针提供特化
 */
template <class T>
struct iterator_traits<const T*>
{
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef ptrdiff_t                  difference_type;
  typedef const T*                   pointer;
  typedef const T&                   reference;
};
/**
 *  返回迭代器类别
 */
template <class Iterator>
inline typename iterator_traits<Iterator>::iterator_category
iterator_category(const Iterator&)
{
  typedef typename iterator_traits<Iterator>::iterator_category category;
  return category();
}
/**
 * 返回表示迭代器距离的类型
 */
template <class Iterator>
inline typename iterator_traits<Iterator>::difference_type*
distance_type(const Iterator&)
{
  return static_cast<typename iterator_traits<Iterator>::difference_type*>(0);
}
/**
 * 返回迭代器所指对象的类型
 */
template <class Iterator>
inline typename iterator_traits<Iterator>::value_type*
value_type(const Iterator&)
{
  return static_cast<typename iterator_traits<Iterator>::value_type*>(0);
}

萃取型别的运行机制:

上述萃取类不但能萃取出了迭代器的型别value_type,还有:

iterator_category;

difference_type;

pointer;

reference;

iterator与iterator_traits的整体运行机制:

各式各样的traits以及对应的头文件(GNU C++):

type traits : .../c++/type_traits

iterator traits: .../c++/bits/stl_iterator.h

char traits: .../c++/bits/char_traits.h

allocator traits:.../c++/bits/alloc_traits.h

pointer traits: .../c++/bits/ptr_traits.h

array traits:.../c++/bits/array.h

算法(algorithms)在操作容器(Container)中的数据时需要通过Iterator知道的如下信息:

iterator_category:Iterator的类别,五种类型之一;

difference_type:两个Iterator之间的距离的type(int、unsigned int),决定了容器可以容纳多少元素;

value_type:容器元素本身的type;

reference:引用;

pointer:指针;

为了符合规范,任何迭代器都应该提供五个内嵌型别,以利于Traits萃取。STL提供了一份iterator class如下:

// 为避免挂一漏万,每个新设计的迭代器都必须继承自它
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;
};

还有与迭代器相关的函数:

distance()用来计算两个迭代器之前的距离。
Advance()有两个参数,迭代器p和n,函数内部将p前进n次。

STL有了迭代器、迭代器萃取器,其算法便可以独立并作用于容器了:

ref:

ZeeCoder:带你深入理解STL之迭代器和Traits技法

OriginalS_TZ:C++ STL之Traits

-End-

相关推荐

Star 17.3k!给它一张屏幕截图,即可一键克隆网页!

本文为大家分享一款本周爆火的GPT开源项目。前言你敢信,只凭借着一张屏幕截图即可转换生成HTML/TailwindCSS代码。可以算得上是前端工程师的福音。它就是screenshot-to-...

AI从截图直接生成代码、前端程序员的福利!

简介项目可以将任何屏幕截图或设计转换为干净的代码(支持大多数框架)。来自领先公司的开发人员和设计师使用的排名第一的工具。完全开源,在GitHub上拥有超过35,000颗星。非常受欢迎。各位小伙...

一款高颜值、跨平台、自托管的免费开源CRM项目——Twenty

前言大家好,这里是可爱的Cherry。作为一个“甲方”,Cherry其实挺知道客户管理的重要的。但是对于客户管理怎么做,以及CRM的作用,我却是一无所知。之前有朋友在评论区留言,说有没有开源的CRM系...

解放双手,前端界面再也不用自己写了?

随着AI技术的发展,现在有越来越多的尝试将AI应用于UI设计和开发中,以期提高效率和降低成本。今天就给大家介绍一个开源的AI网页生成工具:OpenUIOpenUIOpenUI是一个创...

代码调试,教给你(代码调试是什么意思)

昨天我和一些朋友一起调试代码,他们做程序员这一行都不太久,我向他们展示了一些代码调试技巧。今天早上我在想,我应该如何教授他们学习代码调试?我在Twitter上发了一条推文说,我从来没有见过任何好的调试...

Screenshot-to-code:用屏幕截图生成代码

Screenshot-to-code是一个简单的工具,可使用AI将屏幕截图、模型和Figma设计转换为干净、实用的代码。现在支持ClaudeSonnet3.5和GPT-4o!Scre...

next实现原理(next method)

Next.js是一个基于React的服务器端渲染(SSR)和静态生成(SSG)框架,它的实现原理涉及多个关键技术点,包括服务端渲染(SSR)、静态生成(SSG)、客户端渲染(CSR)、...

可逐步操作的具体流程(可逐步操作的具体流程包括)

假设你是一个单人开发者,使用主流技术栈(React+Node.js+MySQL),以下是详细步骤:---###**一、需求分析与原型设计**1.**核心功能清单**-用户能添加、删除、...

截图转代码只需1步!你离高效开发只差这款神器

引言在现代前端开发中,将设计稿转换为代码是一个既重要又耗时的环节。手动编写HTML结构、调试CSS样式、调整布局对齐,不仅耗费时间,还容易出错。而Screenshot-to-Code这款革...

web开发 前端 后端(web开发前端后端)

区别:1、前端是指用户可见的界面,而后端是指用户看不到的东西,考虑底层业务逻辑的实现,平台的稳定性、性能等。2、前端开发用到的技术有HTML5、CSS3、JS、jQuery、Bootstrap、Nod...

手把手教你Dify私有化部署,打造专属AI平台

一、Dify是什么?Dify是一款极具创新性的开源LLM应用开发平台,它就像是一把万能钥匙,为开发者们打开了通往生成式AI应用开发新世界的大门。其融合了后端即服务(BackendasS...

前后端分离架构设计:提升开发效率与业务支撑力的密钥

前后端分离架构设计解析一、定义与核心思想前后端分离是一种将用户界面(前端)与业务逻辑(后端)解耦的架构模式,通过RESTfulAPI或GraphQL实现数据交互。前端专注于视图渲染与交互逻辑...

Kubernetes最小部署单元Pod(kubernetes最小部署单元)

一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...

【程序员必藏!零基础本地部署DeepSeek大模型保姆级教程】

为什么选择本地部署?数据安全:敏感代码/业务数据永不外传闪电响应:局域网推理延迟<100ms,告别云端排队深度定制:自由修改模型代码,打造专属AI助手准备工具(5分钟搞定)1核心工具下载...

【Python程序开发系列】使用Flask实现前后端分离(案例)

这是我的第398篇原创文章。一、引言随着web开发的不断发展,前后端分离已成为越来越流行的架构设计。Flask是一个轻量级的Pythonweb框架,非常适合用于构建API,然后配合前端框...

取消回复欢迎 发表评论: