C++: Essential C++ 读书笔记(chapter3):泛型编程风格概要

本章节主要从以下几个方面做简要分析

  1. 一开始写一个函数,它可以找出vector内小于10的所有元素,然后函数过于死板,没有弹性。
  2. 接下来,我为函数加上了一个数值参数,让用户得以指定某个数值,以此和 vector中的元素做比较。
  3. 接着,我又加上一个新参数,一个函数指针,让用户得以指定比较方式(将类型 equality比较操作符赋予不同的意义)
  4. 然后,我引入function object的概念,使我们得以将某组行为传给函数,此法比函数指针的做法效率更高,并且简单地检阅了标准库提供的 function object。
  5. 最后,我将函数以 function template的方式重新实现,为了支持多种容器,我传入一对 iterator,标示出一组元素范围,为了支持多种元素类型,我将元素类型参数化,也将应用于元素上的 比较操作参数化,以便得以同时支持函数指针和 function object两种方式。

下面我们就按照上述步骤,一步一步学习C++泛型编程思想。 

1:写一个简单函数:在vector容器内完成给定数值的搜索

// iterative version of find() in Section 3.1
// 给定一个储存整数的 vector,以及一个整数值,如果此值存在于 vector内,我们需要
// 返回一个指针指向改值,反之,则返回0,表示给定的值并不在 vector内
const int*  find_ver1(const vector<int>& vec, int value)
{

    for (int ix = 0; ix < vec.size(); ++ix)
    {
        if (vec[ix] == value) {
            return &vec[ix];
        }
    }
    return 0;
}

2:想办法让该函数 可以处理其他数据类型

// 在步骤1中,我们发现这个函数只能处理整数类型,下面我们有一个新任务,就是可以处理
// 任何数据类型(前提是该类型定义有 equality相等运算符)
// 其实这个就是:将泛型算法 find() 以 function template的形式呈现。

template <typename elemType>
elemType*  find_ver2(const vector<elemType> &vec , const elemType& value)
{

    for (int ix = 0; ix < vec.size(); ix++) {
        if(vec[ix] == value) {
            return &vec[ix];
        }
    }
    return 0;
}

2.1 :  find()函数能同时处理 array和 vector容器 

  •  现在我们仍然碰到这样一个问题:这个函数不能同时处理 array 和 vector容器,可能你脑海首先想到
  • 是将此函数重载(overload),一份用来处理 vector 一份用来处理 array。
  • 但是,我们并不想这样做。

那么除了通过重载的策略之外,我们还有其他什么策略呢?下面三个步骤或许可以引起你某些思考。

  1. 将 array的元素传入 find() ,而非指明该 array 。
  2. 将 vector的元素传入find() ,而不指明 vector。
  3. find()增加一个参数size ,表明 array 或者 vector容器的大小,或者传入另一个地址,指示读取操作的终点(我们将此值称为 标兵)。

那么函数原型就是:

解法一:直接传入容器大小 size

template <typename elemType>

elemType* find(const elemType* array ,int size , const elemType & value)

解法二:传入另一个地址,指示 array读取操作的终点(我们将此值称为 标兵)

template <typename elemType>

elemType* find(const elemType* array,  const elemType*  sentinel,const elemType &value);

很显然:上述修改方法:让 array 或者 vector从参数列表中彻底消失了,这解决了我们第一个问题。

另外一个问题就是:由于传递给 find() 的array 是以其第一个元素的指针传入,那么我们应该如何通过特定位置来进行元素的访问了?

下标(subscript)-----》对指针使用下标运算符

下标运算符访问 array每个元素,其实就是将 array的起始地址加上索引值后,产生某个元素的地址,然后该地址被提领(dereference)以返回元素。

注意:在指针的算术运算符中,会把“指针所指的类型”的大小考虑进去。

假设: array所存储的元素是:int 类型, array+2 就是在 array首地址(假设1000)的基础上加2,如果机器的int 长度是 4 byte ,那么这个指针运算符 (array+2) 的答案便是:1008

另一个实现就是:在每次循环迭代中,将 array的值递增 1

for(int ix = 0; ix < size ; ++ix, ++ array

// 通过下标操作符,来进行指针运算
template<typename elemType>
find_ver3 (const elemType* array, int size, const elemType& value)
{
    if(!array || size <1) {
        return 0;
    }
    for(int ix = 0; ix< size; ++ix)
    {
        // we can apply subscript operator to pointer

        if (array[ix] == value) 
           return &array[ix];
    }
    // value not found
    return 0;  
}
// 在每一次的循环中,将 array指针自增

template<typename elemType>
find_ver4(const elemType* array, int size, const elemType& value)
{
    if(!array || size <1) return 0;
    
    // ++array increments array by one element
    for(int ix = 0; ix< size; ++ix, ++array){
        
         // *array dereference the address
         if (*array == value) return array;
    }
    return 0;
}

2.2 :find()函数的size参数通过指针替代 

现在我们再来仔细思考一下:针对 size ,我们是否可以通过另外一个指针来替代了?

答案当然是可以的。这个版本,我们就来实现:通过指针来替代参数 size,扮演标兵的角色。 

template <typename elemType>
elemType* find_ver5(const elemType* first, const elemType* last, const elemType& value)
{
    if(!array || !last) return 0 ;
    
    // 当 first不等于 last,就把value拿来和 first所指的元素进行比较
    // 如果两者相等,便返回 first ,否则将 first递增1,另它指向下一个元素

    for(; first != last;  ++first)
    {

        if (*first == value) return first;
     
    }
    return 0;
}

总结:

    很显然,我们实现了不论数组或者vector 存储的元素类型是什么,我们都可以访问数组中的每个元素。使用方法如下:

int ia[6] = {1,2,3,4,5,6};

string sa[4]  = {"pooh", "piglet", "eeyore", "tigger"};

 int* pi = find(ia, ia+6, ia[2]) 

string* ps = find(sa, sa+4, sa[1]);

2.3:find()函数处理:诸如list类型 (元素存储在非连续内存区域中) 

很显然前面的2.1/2.2章节都是讲的,find()函数处理诸如 vector,array等元素被存储在连续内存区域中,那么针对 list类型(元素存储在非连续内存区域中),上述指针运算符的规则就不适用了。

题外话:

     list也是一个容器,不同的是,list的元素是以一组指针相互链接(linked) :前向(forward)指向下一个(next)元素,后向指针(backward)指针指向上一个(preceding)元素。

思考:

      既然底层指针运算符不适用 list类型,那么我们应该如何做,才能达到上述 vector/ array容器的效果了?

解决方案:

     解决这个问题的方法是:在底层指针的行为之上提供一层抽象,取代程序原本的“指针直接操作方式”。我们可以把底层指针的处理通通放在此抽象层总,让用户无需面对指针的操作。

抽象层实现

      那么如何实现了? 是的,我们需要一组对象可提供如内置运算符(++,* ,==, !=) 的一般运算符,我们可以利用C++类机制来达到目的。

      我们先说一下结论:

      泛型指针 iterator可以达到这个目的,我们首先来看一下:标准容器的 iterator 。

     那么如何取得 iterator呢? 其实每个标准容器都提供一个名为  begin() 的操作函数,它可返回一个 iterator ,指向第一个元素,另一个 end()的操作函数,它返回的 iterator指向最后一个元素的下一个位置。不论如何定义 iterator对象 ,

以下都是对 iterator进行赋值(assign),比较(compare) ,递增(increment),提领(dereference)操作

for (iter = sevc.begin() ;  iter != sevc.end(); ++ iter) {

        cout << *iter << " ";

 }

在说一下设计思想:

        不管何种容器(vector或者list) 他们的 iterator迭代器,都会实现上述基本的运算,我们可以大胆猜测,这些基本的运算是通过 iterator 的 inline内联函数实现的,不同的是:

vector :  递增(increment)操作是在目前的地址上直接加上一个元素的大小

list : 递增(increment)操作 是:沿着list指针前进到下一个元素。

总结:

     所以如果定义一个iterator ,我们首先要考虑这两个方面:

  1. 迭代对象(某个容器自身类型)的类型,这是决定如何访问下一个元素关键
  2. iterator所指的元素类型(容器存类型)这是决定iterator 提领操作的返回值
// 下面我来重新实现 find()函数,让它同时支持这两种形式:一对指针,或是一对指向某种容器的 iterator

template<typename IteratorType, typename elemType>
find_ver6(IteratorType first, IteratorType last, const elemType& value)
{
    for(; first != last; ++first){
        if(value == *first) return first;
    }
    // last 是最后一个元素的后面一个
    return last;
}

// 现在我们来看看,array , vector, last该如何使用它

const int size = 8;
int ia {size} = {1,1,2,3,5,8,13,21};

// 以ia的 8个元素作为 list 和 vector的初值
vector<int> ivec(ia, ia+size);
list<int> ilist(ia, ia + size);

int *pia = find(ia, ia+size, 13);
if (pia != ia+ size) {
    // 找到了 元素 13
    .........
}

vector<int>:: iterator it;
it = find(ivec.begin(), ivec.end(), 13);
if (it != ivec.end()){
    // 找到了元素13
    .......
}

list<int>::iterator iter;
iter = find(ilist.begin(), ilist.end(), 13) 
if(iter != ilist.end()){
    // 找到了元素13
    .......
}


 其实优化到 find_ver6() 这一步,find函数本身就已经有了很大的通用性,远远超过我们的想象,但是故事还没结束。

思考:

     find()函数实际上是:使用了底部元素所属类型的 equality(相等)运算符,如果底部元素所属类型没有提供这个 相等运算符,或者用户希望这个运算符具备其他的意义,那么这个find()函数的弹性就不够了,如何在这样的场景下增加 find()函数的弹性了?

答案:

  1.     就是传入一个指针,取代原本固定的相等运算符。
  2.      或者运用所谓的 function object(一种特殊的 class) 。

我们下一个努力目标就是将刚刚现有的 find() 版本演化为 泛型算法(比如:标准库提供的 find_if() 能够接受函数指针或 function object, 取代底部元素的 equality运算符,大大提升弹性。) 

总共有60个泛型算法:

  1. 搜索算法(search algorithm):find() , count(), adjacent_find(),  find_if() , count_if(),  binary_search(),  find_first_of()。
  2. 排序算法(sorting)以及次序整理(ordering)算法: merge(),partial_sort(),partition(),random_shuffile(),reverse(),rotate(),sort()。
  3. 复制(copy)、删除(delete)、替换(substiution)算法:copy(),remove(),remove_if(),replace(),replace_if(),swap(),unique()。
  4. 关系算法(relational):equal(),includes(),mismatch()。
  5. 生成(generation)与质变(mutation)算法:fill(),for_each(),generate(),transform()。
  6. 数值(numeric)算法:accmulate(),adjance_difference(),partial_sum(),inner_product()。
  7. 集合算法(set):set_union(),set_difference()。

所有容器的共通操作

下列为所有容器类(包括string类)的共通操作

  1. equality(==)和 inequality(!=) 运算符, 返回 true 或者 false。
  2. assigment(=)运算符,将某个容器复制给另一个容器。
  3. empty() 会在容器无任何元素时返回 true,否则返回 false。
  4. size() 返回容器内目前持有的元素个数。
  5. clear() 删除所有元素。
  • 事实上每个容器都提供了 begin()和end()两个函数,他们分别返回第一个元素和最后一个元素的下一个位置的 iterator。
  • begin() 返回一个 iterator指向容器的第一个元素
  • end()返回一个 iterator指向容器的最后一个元素的下一个位置
  •  通常我们在容器上进行的迭代操作都是始于 begin(),终于end(),所有容器都提供insert()用以插入元素,以及 erase()用以删除元素。
  • insert()将单一或某个范围的元素插入容器内
  • erase()将容器的单一元素或某个范围内的元素删除。

3:给find()函数加上一个新参数,一个函数指针,让用户得以指定比较方式(将类型 equality比较操作符赋予不同的意义) 

具体来说就是:设计一个算法,既可以实现 less_than也可以实现 greater_than比较。 

案例背景:

   下面我们有一个新任务,用户给予一个整数 vector,我们必须返回一个新的 vector,其中内含原 vector之中所有小于10的所有数值,那么显而易见一个快速但缺乏弹性的解法就是: 

vector<int> less_than_10(const vector<int>& vec)
{
    vector<int> nvec;
    for (int ix = 0; ix< vec.size(); ++ix) {
        if (vec[ix] < 10) {
            nvec.push_back(vec[ix]);
        }
    }
    return nvec;
}

1:如果用户想要找出所有小于 11 的元素,应该如何做了 ?

    我们就给这个函数增加一个参数,用户指定需要比较的数值。

2:如果用户想要指定不同的比较操作,比如:大于,小于,等于,那么如何才能将“比较操作”参数化呢 ?

   用函数指针来取代 less_than比较运算符。

   即我们可以在问题1的基础上加入 第三个参数 pred,用它来指定一个函数指针(它的参数列表有两个整数,返回值为bool)并将less_than函数用 filter()取代。

3:函数原型

vector<int>  filter(const vector<int>& vec , int filter_value, bool (*pred)(int, int ));

站在用户使用的角度考虑,为了方便起见,我们同时定义了许多可传给 filter() 的关系比较函数:

bool less_than(int v1, int v2) {

        return v1< v2 ? true : false

bool greates_than(int v1, int v2) {

        return v1> v2 ? true: false

}

有了上述函数原型后,接下来的工作便是需要去实现filter()函数。

vector<int> filter_ver1(const vector<int>& vec, int filter_value, bool (*pred)(int, int))
{
    vector<int> new_vec;
    for (int ix= 0; ix < vec.size(); ++ix) {
        // 调用pred所指函数
        // 比较 vec[ix] 和 filter_value 
        
        if (pred(vec[ix], filter_value)) {

            new_vec.push_back(vec[ix]);
        }
    }
    return new_vec;
}

4:引入function object的概念(函数对象),来减少函数指针对函数的调用而带来的性能消耗。

4.1: 函数对象 function object替换函数指针

概念:

     所谓 function object 是某种class实例对象,这类class对 function call 运算符做了重载操作,如此一来,function object就可以被当成一般函数来使用了。

     其实 function object 实现了我们原本可能以独立函数加以定义的事物,但是何必如此了?

其实主要是为了效率,我们可以令 call 运算符称为 inline,从而消除“通过函数指针来调用函数”时需付出的额外代价。

总结:

     1:在类中重载定义成员函数 operator() ,这个类就称为函数对象类。

      2:可以作为函数的参数(例如传递给标准算法的谓词或比较函数)。

示例:

     struct myFunctionObject {

                // 重载 operator()操作符,使 myFunctionObject变成函数对象

                int operator()(int a) {

                       return a;

                }

     } myobject;

     int x = myObject(1);

     struct Mygreater {

              // 重载 operator() 操作符,并实现降序排序,使 Mygreater变成函数对象

              bool operator()(const int& x ,  const int& y) {

                        return x>y;

              }

     }mygreater;

     vector<int> vec;

      // 将 mygreater作为函数参数传递给sort排序函数

     sort (vec.begin(), vec.end(), mygreater());

 扩展:

       在标准库中事先定义 一组 function objcet,主要分为:算术运算(arithmetic) ,关系运算(relational)和逻辑运算(logical)这三大类。以下列表中的type在实际使用时会替换为内置类型或 class类型。

       在使用时,需要事先包含头文件 <functional>

6个算术运算

plus<type>,minus<type>,negate<type>,

multiplies<type>,divides<type>,modules<type>

6个关系运算

less<type>,less_equal<type>,greater<type>,

greater_equal<type>,equal_to<type>,not_equal_to<type>

3个逻辑运算logical_and<type>,logical_or<type>,logic_not<type>

4.2:使用泛型算法 transform()实现数据转换 

现在我们在思考一个问题:

       我们需要略加变化的方式来显示 Fibonacc 数列: 比如,没给元素和自身相加,和自身相乘,被加到对应的 Pell数列等等。做法之一就是 使用泛型算法 transform() 并搭配 plus<int> 和 multiplus<int>  。

      transform()定义

          function templat<algorithm> std:: transform

      一元操作

template <class InputIterator, class OutputIterator, class UnaryOperation>

OutputIterator transform(InputIterator first1, InputIterator last1,

            OutputIterator result, UnaryOperation op);

将 op操作算法应用于[first1,last1]范围内的每个元素,并将每个操作返回的值存储在从 result开始的范围内。

  二元操作

    template <class InputIterator1, class InputIterator2,class OutputIterator, class BinaryOperation>

OutputIterator transform(InputIterator1 first1, InputIterator1 last1,

       InputIterator2 first2, OutputIterator result,

                         BinaryOperation binary_op);

  使用范围[first1, last1]中的每个元素作为第一个参数,并使用范围中从 first2开始的各个参数作为第二个参数来调用  binary_op 。每个调用返回的值将存储在从result开始的范围内。

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值